diff --git a/.github/workflows/ci-autofix.yaml b/.github/workflows/ci-autofix.yaml index 3baae89..1a94d9b 100644 --- a/.github/workflows/ci-autofix.yaml +++ b/.github/workflows/ci-autofix.yaml @@ -9,6 +9,12 @@ name: CI — Autofix # head, then set output committed=true so the caller skips portions on SHA_old. # Do not --no-verify. Do not push to main. # +# Presets run first, then any format-command / lint-fix-command / extra-command. +# prettier npm ci + npm run format (requires package-lock.json) +# eslint npm ci + npx eslint . --fix (opt-in; do not call npm run lint) +# ruff ruff format . + ruff check --fix . (ruff 0.15.22) +# terraform terraform fmt -recursive in terraform-working-directory +# # Caller example: # jobs: # autofix: @@ -17,38 +23,52 @@ name: CI — Autofix # permissions: { contents: write } # secrets: inherit # with: -# format-command: npm run format -# lint-fix-command: npm run lint -- --fix +# presets: prettier,terraform +# +# Python callers pass presets: ruff,terraform. Add eslint only when that +# repo's CI lint step is ESLint itself and Prettier owns formatting. +# Do not pass `npm run lint -- --fix` (some apps chain Redocly into lint). # # Org secrets (names only): AUTOFMT_APP_ID, AUTOFMT_APP_PRIVATE_KEY. on: workflow_call: inputs: - format-command: - description: "Write formatter command (e.g. npm run format, ruff format .)" + presets: + description: "Comma-separated presets: prettier, eslint, ruff, terraform" type: string - required: true + required: false + default: "" + format-command: + description: "Optional write command run after presets (e.g. npm run format)" + type: string + required: false + default: "" lint-fix-command: - description: "Optional write lint-fix command (e.g. ruff check --fix .)" + description: "Optional write lint-fix command run after presets" type: string required: false default: "" extra-command: - description: "Optional extra write command (e.g. terraform fmt -write)" + description: "Optional extra write command run after presets" type: string required: false default: "" node-version: - description: "Node.js version when package-lock.json is present" + description: "Node.js version for the prettier or eslint preset, or an npm command" type: string required: false default: "24" terraform-version: - description: "Terraform version when extra-command mentions terraform" + description: "Terraform version for the terraform preset or an extra-command that runs terraform" type: string required: false default: "1.16.0" + terraform-working-directory: + description: "Directory for the terraform preset (terraform fmt -recursive)" + type: string + required: false + default: "terraform" outputs: committed: description: "true when this job pushed a formatter commit" @@ -105,27 +125,93 @@ jobs: ref: ${{ github.head_ref }} persist-credentials: true + - name: Resolve presets + id: presets + env: + SKIP_BOT: ${{ steps.skip-bot.outputs.skip }} + PRESETS: ${{ inputs.presets }} + FORMAT_COMMAND: ${{ inputs.format-command }} + LINT_FIX_COMMAND: ${{ inputs.lint-fix-command }} + EXTRA_COMMAND: ${{ inputs.extra-command }} + run: | + set -euo pipefail + + write_outputs() { + { + echo "prettier=$1" + echo "eslint=$2" + echo "ruff=$3" + echo "terraform=$4" + } >> "${GITHUB_OUTPUT}" + } + + if [ "${SKIP_BOT}" = "true" ]; then + write_outputs false false false false + exit 0 + fi + + want_prettier=false + want_eslint=false + want_ruff=false + want_terraform=false + + if [ -n "${PRESETS}" ]; then + IFS=',' read -ra parts <<< "${PRESETS}" + for raw in "${parts[@]}"; do + token=$(printf '%s' "${raw}" | tr -d '[:space:]') + case "${token}" in + "") ;; + prettier) want_prettier=true ;; + eslint) want_eslint=true ;; + ruff) want_ruff=true ;; + terraform) want_terraform=true ;; + *) + echo "Unknown preset: ${token}" >&2 + exit 1 + ;; + esac + done + fi + + if { [ "${want_prettier}" = "true" ] || [ "${want_eslint}" = "true" ]; } && [ ! -f package-lock.json ]; then + echo "prettier and eslint presets require package-lock.json" >&2 + exit 1 + fi + + if [ "${want_prettier}" = "false" ] \ + && [ "${want_eslint}" = "false" ] \ + && [ "${want_ruff}" = "false" ] \ + && [ "${want_terraform}" = "false" ] \ + && [ -z "${FORMAT_COMMAND}" ] \ + && [ -z "${LINT_FIX_COMMAND}" ] \ + && [ -z "${EXTRA_COMMAND}" ]; then + echo "Set presets or a format, lint-fix, or extra command." >&2 + exit 1 + fi + + write_outputs "${want_prettier}" "${want_eslint}" "${want_ruff}" "${want_terraform}" + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - if: ${{ steps.skip-bot.outputs.skip != 'true' && hashFiles('package-lock.json') != '' }} + if: ${{ steps.skip-bot.outputs.skip != 'true' && hashFiles('package-lock.json') != '' && (steps.presets.outputs.prettier == 'true' || steps.presets.outputs.eslint == 'true' || contains(inputs.format-command, 'npm') || contains(inputs.lint-fix-command, 'npm')) }} with: node-version: ${{ inputs.node-version }} cache: npm - name: Install npm dependencies - if: ${{ steps.skip-bot.outputs.skip != 'true' && hashFiles('package-lock.json') != '' }} + if: ${{ steps.skip-bot.outputs.skip != 'true' && hashFiles('package-lock.json') != '' && (steps.presets.outputs.prettier == 'true' || steps.presets.outputs.eslint == 'true' || contains(inputs.format-command, 'npm') || contains(inputs.lint-fix-command, 'npm')) }} run: npm ci - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 - if: ${{ steps.skip-bot.outputs.skip != 'true' && (contains(inputs.format-command, 'ruff') || contains(inputs.lint-fix-command, 'ruff')) }} + if: ${{ steps.skip-bot.outputs.skip != 'true' && (steps.presets.outputs.ruff == 'true' || contains(inputs.format-command, 'ruff') || contains(inputs.lint-fix-command, 'ruff') || contains(inputs.extra-command, 'ruff')) }} with: python-version: "3.12" - name: Install ruff - if: ${{ steps.skip-bot.outputs.skip != 'true' && (contains(inputs.format-command, 'ruff') || contains(inputs.lint-fix-command, 'ruff')) }} + if: ${{ steps.skip-bot.outputs.skip != 'true' && (steps.presets.outputs.ruff == 'true' || contains(inputs.format-command, 'ruff') || contains(inputs.lint-fix-command, 'ruff') || contains(inputs.extra-command, 'ruff')) }} run: pip install 'ruff==0.15.22' - uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1 - if: ${{ steps.skip-bot.outputs.skip != 'true' && contains(inputs.extra-command, 'terraform') }} + if: ${{ steps.skip-bot.outputs.skip != 'true' && (steps.presets.outputs.terraform == 'true' || contains(inputs.extra-command, 'terraform')) }} with: terraform_version: ${{ inputs.terraform-version }} terraform_wrapper: false @@ -133,12 +219,32 @@ jobs: - name: Apply formatter if: ${{ steps.skip-bot.outputs.skip != 'true' }} env: + PRETTIER: ${{ steps.presets.outputs.prettier }} + ESLINT: ${{ steps.presets.outputs.eslint }} + RUFF: ${{ steps.presets.outputs.ruff }} + TERRAFORM: ${{ steps.presets.outputs.terraform }} + TERRAFORM_DIR: ${{ inputs.terraform-working-directory }} FORMAT_COMMAND: ${{ inputs.format-command }} LINT_FIX_COMMAND: ${{ inputs.lint-fix-command }} EXTRA_COMMAND: ${{ inputs.extra-command }} run: | set -euo pipefail - bash -euo pipefail -c "${FORMAT_COMMAND}" + if [ "${PRETTIER}" = "true" ]; then + npm run format + fi + if [ "${ESLINT}" = "true" ]; then + npx eslint . --fix + fi + if [ "${RUFF}" = "true" ]; then + ruff format . + ruff check --fix . + fi + if [ "${TERRAFORM}" = "true" ]; then + terraform -chdir="${TERRAFORM_DIR}" fmt -recursive + fi + if [ -n "${FORMAT_COMMAND}" ]; then + bash -euo pipefail -c "${FORMAT_COMMAND}" + fi if [ -n "${LINT_FIX_COMMAND}" ]; then bash -euo pipefail -c "${LINT_FIX_COMMAND}" fi diff --git a/README.md b/README.md index 6925cb5..a4afc29 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ The formatter GitHub App is not on the main-branch bypass list. **`.github/workflows/ci-terraform.yaml`** — Terraform `fmt -check`, `init -backend=false`, and `validate`. Default version `1.16.0`. -**`.github/workflows/ci-autofix.yaml`** — Pull-request-only formatter. Mints a GitHub App token (`AUTOFMT_APP_ID`, `AUTOFMT_APP_PRIVATE_KEY`), runs the caller's write commands, and pushes `style: apply formatter` only when the tree is dirty. Output `committed` lets the caller skip portions on SHA_old. Does not `--no-verify` and does not push to `main`. +**`.github/workflows/ci-autofix.yaml`** — Pull-request-only formatter. Mints a GitHub App token (`AUTOFMT_APP_ID`, `AUTOFMT_APP_PRIVATE_KEY`), runs the requested presets and any optional write commands, and pushes `style: apply formatter` only when the tree is dirty. Presets are `prettier` (`npm run format`), `eslint` (`npx eslint . --fix`, opt-in), `ruff` (`ruff format .` and `ruff check --fix .`), and `terraform` (`terraform fmt -recursive` in `terraform-working-directory`, default `terraform`). Output `committed` lets the caller skip portions on SHA_old. Does not `--no-verify` and does not push to `main`. **`.github/workflows/cd-hcp-fargate.yaml`** — HCP Fargate image CD. Checkout at `ref` (empty means `github.sha`), OIDC, SSM cluster/service/family/ecr/container/api-url, docker build+push tagged `$sha` and `$environment`, patch `GIT_SHA`, RegisterTaskDefinition + UpdateService + services-stable, poll health SHA. `environment` is a `with:` input. The reusable job owns `environment:`, concurrency, OIDC, and `vars.DEPLOY_ROLE_ARN`. @@ -268,8 +268,7 @@ jobs: permissions: { contents: write } secrets: inherit with: - format-command: npm run format - lint-fix-command: npm run lint -- --fix + presets: prettier,terraform frontend: needs: autofix @@ -304,7 +303,7 @@ jobs: test "${TERRAFORM}" = success ``` -Python HCP callers pass `format-command: ruff format .` and `lint-fix-command: ruff check --fix .`. Optional `extra-command: terraform fmt -write` is available on `ci-autofix.yaml`. Do not run `eslint --fix` unless that repo's `lint` script is already fix-safe. Flip org ruleset membership in the same window as this merge: include on `CI complete`, exclude from `main branch protection`. Never require both `ci / ci` and `ci-complete`. Do not edit native GitHub merge-queue rulesets. +Python HCP callers pass `presets: ruff,terraform`. The `eslint` preset is opt-in and runs `npx eslint . --fix`. Do not pass `npm run lint -- --fix`: several apps chain Redocly into `lint`. Enable `eslint` only when that repo's CI lint step is ESLint itself and Prettier owns formatting. Optional `format-command`, `lint-fix-command`, and `extra-command` still run after the presets. Flip org ruleset membership in the same window as this merge: include on `CI complete`, exclude from `main branch protection`. Never require both `ci / ci` and `ci-complete`. Do not edit native GitHub merge-queue rulesets. ### 3. Add CD to a repo diff --git a/workflow-templates/ci-hcp.yml b/workflow-templates/ci-hcp.yml index 79df36b..ac655d2 100644 --- a/workflow-templates/ci-hcp.yml +++ b/workflow-templates/ci-hcp.yml @@ -17,8 +17,7 @@ jobs: contents: write secrets: inherit with: - format-command: npm run format - lint-fix-command: "npm run lint -- --fix" + presets: prettier,terraform frontend: needs: autofix