Skip to content

feat: add shared composite-action layer (setup-project + lint/test mix-ins)#5

Merged
RobEasthope merged 4 commits into
mainfrom
sk-440-author-the-shared-composite-action-layer-setup-project
Jun 26, 2026
Merged

feat: add shared composite-action layer (setup-project + lint/test mix-ins)#5
RobEasthope merged 4 commits into
mainfrom
sk-440-author-the-shared-composite-action-layer-setup-project

Conversation

@RobEasthope

@RobEasthope RobEasthope commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

What

Authors Layer 1 of ADR 0001 (§5.1–§5.3, §5.7): the granular composite actions under .github/actions/ that the coarse reusable workflows (SK-415 lint.yml, SK-416 build-test.yml) will compose. Splitting at the action layer is free (steps in the calling job — no extra runner/install), so pick-and-mix lives here whilst the reusable workflows stay coarse and pay setup once.

Closes the full SK-440 checklist:

Action Purpose
setup-project pnpm + Node-from-.nvmrc + restore-only pnpm store cache, then install (the keystone)
eslint / lint-markdown / lint-yaml lint mix-ins
typecheck / test-vitest / test-bats build/test mix-ins
shellcheck / changelog-validate infra/changelog mix-ins

Notable design points

  • SK-438 config injectionlint-yaml runs yamllint with -c pointed at this repo's own .yamllint.yml, resolved relative to github.action_path ($GITHUB_ACTION_PATH/../../../.yamllint.yml). Consumers carry no local copy; the root file stays the single source of truth.
  • No drift — tool pins (pnpm v4.4.0, setup-node v6.4.0, yamllint 1.37.1, actionlint 1.7.5 installer SHA) mirror ci.yml exactly.
  • Author-only — this repo does not dogfood the actions via uses: (same sha_pinning_required reason as the inline workflows). They're consumed cross-repo by @<sha>; documented in .github/actions/README.md and CLAUDE.md.
  • SK-422 trust boundary respected — every third-party uses:/installer SHA-pinned, set -euo pipefail, composite actions inherit the job's permissions.

Verification

  • prettier --check, yamllint, markdownlint-cli2, actionlint all green.
  • ✅ Verified lint-yaml's injected-config path resolution + run, the lint-markdown glob, and shellcheck's graceful no-scripts skip locally.
  • ⚠️ Full act integration smoke-test (the setup-project install path) was not run — Docker wasn't available in the authoring environment.
  • ℹ️ eslint/typecheck/test-vitest/test-bats/changelog-validate have no local fixtures in this repo (it ships workflows, not a package), so they're authored to house style; real validation lands when SK-415/416 wire them into consumers. changelog-validate's default command (pnpm run changelog:validate) should be confirmed against the estate's actual dated-changelog tool.

Refs SK-440.

Summary by CodeRabbit

  • New Features
    • Added a set of reusable CI actions for setup, linting, type checking, and test runs.
    • Introduced changelog validation and shell script checks for more consistent repository hygiene.
  • Documentation
    • Expanded guidance on how the shared CI actions are organised and used.
    • Clarified configuration, pinning, and workflow composition conventions.
  • Bug Fixes
    • Improved handling of command inputs, file globs, and empty file sets for more reliable checks.

@claude

claude Bot commented Jun 26, 2026

Copy link
Copy Markdown

Claude finished @RobEasthope's task in 2m 32s —— View job


Code Review — PR #5

  • Gather context from PR description and prior review comments
  • Read all changed action files
  • Review for correctness, security, and style
  • Post findings

Verified fixes (all three prior blocking issues resolved correctly)

lint-markdown — eval removed
The while IFS= read -r glob; done <<<"$MD_GLOBS" approach correctly splits on newlines, not shell tokens — no injection surface, works in bash 3+. The [ -n "$glob" ] guard handles trailing blank lines from the herestring correctly. Cleaner than mapfile -t and equally safe.

changelog-validate — eval removed
pnpm run "$CHANGELOG_SCRIPT" passes the script name as a single quoted argument to pnpm. No re-parsing, no injection. The input description and the header comment both document the constraint clearly.

shellcheck — array-based file collection
The rewrite is correct: find … -print0 piped through while IFS= read -r -d '' builds the array without any whitespace hazard. The git ls-files -z default path is equally robust. The "${#files[@]}" -eq 0 empty-check and shellcheck --severity="$SEVERITY" "${files[@]}" final invocation are both right.

setup-project + ADR §5.2 — doc/impl mismatch corrected
The ADR now correctly says "restores at job start and saves in a single post-job step". Keeping cache: pnpm (rather than switching to actions/cache/restore) is the right call at single-package scale — the dedicated-save-job dance is still avoided, and the cache actually gets populated.

ci.yml--break-system-packages parity
Good proactive fix. Keeps the inline CI and the composite action in sync ahead of the ubuntu-latest → 24.04 migration.


Remaining observations (non-blocking)

lint-yaml/action.yml:81./actionlint written to workspace root
The installer drops the binary into CWD (the consumer's repo root). ./actionlint -color then executes it. This is the standard installation pattern from rhysd/actionlint and works fine on GitHub-hosted runners. Worth knowing if a consumer ever has a file at that path (vanishingly unlikely), but not a concern worth changing.

eslint/action.yml:32 and test-bats/action.yml:37 — unquoted $VAR word-splitting
These are intentional argument pass-through, not eval. Callers are trusted internal estate consumers supplying space-separated paths/flags to their own tooling. The # shellcheck disable=SC2086 annotations and explanatory comments make the intent clear. The injection surface here is qualitatively different from the eval cases that were fixed — no shell re-parsing occurs.

lint-yaml/action.yml version inputs — mutable but documented
The input descriptions now carry "bump deliberately in lockstep, not ad-hoc per consumer (SK-422)". That's the right mitigation given this is an internal estate action. The checksum verification the installer performs on the downloaded binary is the backstop.


Overall

The three major findings from the prior review round are all correctly fixed. The security posture (no eval, array-based file collection, pnpm script names not shell commands) is sound. Documentation is in sync with implementation. No blocking issues remain.

@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@RobEasthope, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 9 minutes and 44 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 79d6701c-6c80-4fb0-adf0-35b5a1fd82ea

📥 Commits

Reviewing files that changed from the base of the PR and between 3e9861b and 81e5d34.

📒 Files selected for processing (13)
  • .github/actions/README.md
  • .github/actions/changelog-validate/action.yml
  • .github/actions/eslint/action.yml
  • .github/actions/lint-markdown/action.yml
  • .github/actions/lint-yaml/action.yml
  • .github/actions/setup-project/action.yml
  • .github/actions/shellcheck/action.yml
  • .github/actions/test-bats/action.yml
  • .github/actions/test-vitest/action.yml
  • .github/actions/typecheck/action.yml
  • .github/workflows/ci.yml
  • CLAUDE.md
  • docs/adr/0001-shared-ci-architecture-for-npm-packages.md
📝 Walkthrough

Walkthrough

Adds a Layer 1 shared-CI action catalogue, new composite actions for setup, linting, typechecking, and tests, plus repository guidance covering pinning, inputs, and the Layer 1/Layer 2 split.

Changes

Layer 1 composite actions

Layer / File(s) Summary
Project setup
.github/actions/setup-project/action.yml
The setup-project composite action installs pnpm, restores the pnpm cache with Node setup, and conditionally runs pnpm install from the new inputs.
Type and test actions
.github/actions/typecheck/action.yml, .github/actions/test-vitest/action.yml, .github/actions/test-bats/action.yml
The Typecheck, Test (Vitest), and Test (Bats) composite actions add configurable inputs and run pnpm exec tsc, vitest, and bats under strict Bash settings.
Lint and validation actions
.github/actions/changelog-validate/action.yml, .github/actions/eslint/action.yml, .github/actions/lint-markdown/action.yml, .github/actions/shellcheck/action.yml
The changelog, ESLint, Markdown, and ShellCheck composite actions add configurable inputs and execute their matching validation commands with strict shell handling.
YAML linting action
.github/actions/lint-yaml/action.yml
The Lint YAML composite action adds path and version inputs, runs yamllint against the shared config, and optionally installs and runs actionlint.
Catalogue and repo guidance
.github/actions/README.md, CLAUDE.md
The action catalogue README and CLAUDE guidance describe the Layer 1 composite-action set, pinning conventions, and the Layer 1/Layer 2 split.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarises the main change: adding a shared composite-action layer with setup-project and lint/test mix-ins.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sk-440-author-the-shared-composite-action-layer-setup-project
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch sk-440-author-the-shared-composite-action-layer-setup-project

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 26, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/actions/lint-markdown/action.yml:
- Around line 11-17: The `globs` input in the `lint-markdown` action is being
treated as shell code instead of plain data. Update the action’s Markdown lint
step to pass `inputs.globs` directly to `markdownlint-cli2` without `eval`, and
keep the input as a plain structured list/value in `action.yml`. If any
repo-specific glob defaults are needed, move them to each consumer’s
`repo-config.yaml` rather than hardcoding or re-parsing them inside the
composite action.

In @.github/actions/setup-project/action.yml:
- Around line 45-50: The setup in the reusable action currently uses
actions/setup-node with cache: pnpm, which performs both restore and save rather
than restore-only. Update the setup-project action to use an explicit
actions/cache/restore step for the pnpm store cache and keep actions/setup-node
focused on Node version setup only. Use the existing setup-node step and the
pnpm cache configuration in this action to locate the change, and remove the
built-in cache flow so the job does not save cache state here.

In @.github/actions/shellcheck/action.yml:
- Around line 27-42: The ShellCheck target collection in the action run script
is still using text-based `ls` and `echo | xargs`, which can mis-handle
directory inputs and filenames with spaces. Update the `run` block to build the
target list as an array in the shellcheck action, using the existing `SC_PATHS`
handling and `git ls-files` output directly without flattening it back into
whitespace-delimited text. Keep the logic in the same shell script section and
ensure the final `shellcheck` invocation consumes the array safely with the
existing `SEVERITY` setting.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f1f88061-5b9d-4997-b343-9ffde06d1ef4

📥 Commits

Reviewing files that changed from the base of the PR and between e03fb24 and 3e9861b.

📒 Files selected for processing (11)
  • .github/actions/README.md
  • .github/actions/changelog-validate/action.yml
  • .github/actions/eslint/action.yml
  • .github/actions/lint-markdown/action.yml
  • .github/actions/lint-yaml/action.yml
  • .github/actions/setup-project/action.yml
  • .github/actions/shellcheck/action.yml
  • .github/actions/test-bats/action.yml
  • .github/actions/test-vitest/action.yml
  • .github/actions/typecheck/action.yml
  • CLAUDE.md

Comment thread .github/actions/lint-markdown/action.yml Outdated
Comment on lines +45 to +50
- name: 🟢 Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: ${{ inputs.node-version-file }}
# Restore-only pnpm store cache, keyed on the lockfile hash.
cache: pnpm

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does the official actions/setup-nodedocumentation say thatcache: pnpm is restore-only, or does it also save the cache in a post-job step? Please cite the relevant docs.

💡 Result:

The official documentation for actions/setup-node does not state that the built-in cache: pnpm functionality is restore-only [1][2]. Instead, when cache: 'pnpm' is configured, the action automatically handles both restoring the cache at the start of the job and saving the cache in a post-job step [1][2]. The actions/setup-node documentation notes that it uses actions/cache under the hood to provide this built-in functionality [1][2]. Because it manages both the restore and save operations automatically, it does not support "restore-only" behavior by default when using the cache input [3]. To achieve a restore-only workflow, the documentation directs users to use actions/cache/restore directly instead of the built-in cache input of actions/setup-node [4][5].

Citations:


actions/setup-node cache is not restore-only. At .github/actions/setup-project/action.yml:45-50, cache: pnpm uses the action’s built-in cache flow, which restores at the start of the job and saves in a post-job step. If this action is meant to be restore-only, switch to an explicit actions/cache/restore step instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/actions/setup-project/action.yml around lines 45 - 50, The setup in
the reusable action currently uses actions/setup-node with cache: pnpm, which
performs both restore and save rather than restore-only. Update the
setup-project action to use an explicit actions/cache/restore step for the pnpm
store cache and keep actions/setup-node focused on Node version setup only. Use
the existing setup-node step and the pnpm cache configuration in this action to
locate the change, and remove the built-in cache flow so the job does not save
cache state here.

Comment thread .github/actions/shellcheck/action.yml Outdated
…x-ins)

Author Layer 1 of ADR 0001 (§5.7): the granular composite actions that the
coarse reusable workflows (SK-415/416) will compose. Splitting at the action
layer is free (steps in the calling job), so pick-and-mix lives here whilst the
reusable workflows stay coarse and pay setup once.

Actions under .github/actions/:
- setup-project: pnpm + Node-from-.nvmrc + restore-only store cache, then install
- eslint, lint-markdown, lint-yaml: lint mix-ins
- typecheck, test-vitest, test-bats: build/test mix-ins
- shellcheck, changelog-validate: infra/changelog mix-ins

lint-yaml implements the SK-438 config injection: yamllint runs against this
repo's own .yamllint.yml (resolved via github.action_path), so consumers carry
no local copy. Tool pins (pnpm, Node, yamllint 1.37.1, actionlint 1.7.5) mirror
ci.yml so the dogfooded self-CI and the shipped actions never drift.

Author-only: this repo does not dogfood the actions via uses: (same
sha_pinning_required reason as the inline workflows). Adds the action catalogue
README and documents the layer in CLAUDE.md.

Refs SK-440.
@RobEasthope
RobEasthope force-pushed the sk-440-author-the-shared-composite-action-layer-setup-project branch from 3e9861b to 54752a9 Compare June 26, 2026 14:03
@RobEasthope

Copy link
Copy Markdown
Contributor Author

✅ Full act integration smoke-test now run (Docker available)

Ran a throwaway workflow (not committed) wiring the dogfoodable actions against this repo — setup-projectlint-yamllint-markdown in one job. Job succeeded end-to-end:

  • setup-project — pnpm v10.33.0 installed, Node set up, pnpm install green.
  • lint-yaml — log confirms the SK-438 config injection works in a real container: Using centralised yamllint config: …/.yamllint.yml; yamllint 1.37.1 ran green, actionlint v1.7.5 downloaded + ran green.
  • lint-markdown — 0 errors.

One fix the smoke-test surfaced

On the catthehacker/ubuntu:act-latest image (Ubuntu 24.04), lint-yaml's python3 -m pip install --user yamllint failed with error: externally-managed-environment (PEP 668). Hardened the install with --break-system-packages — the sanctioned PEP 668 escape hatch, a no-op on the GitHub-hosted runner's Python but required on a distro Python that marks itself externally-managed. Re-ran: green.

⚠️ Parity note for a maintainer

ci.yml's inline yamllint step uses the same bare python3 -m pip install --user yamllint==1.37.1 without --break-system-packages. It works on today's ubuntu-latest, but as that image moves to 24.04 it could hit the same PEP 668 wall. Left untouched here (this PR is author-only and doesn't wire ci.yml), but worth a follow-up to keep the two in sync per the "no drift" rule in CLAUDE.md.

Add --break-system-packages to ci.yml's inline yamllint step so it keeps
working once ubuntu-latest moves to a distro Python that marks itself
externally-managed (Ubuntu 24.04). No-op on the current runner. Mirrors the
lint-yaml composite action added in this PR so the inline copy and the action
never drift.

Refs SK-440.
CHANGELOG_COMMAND: ${{ inputs.command }}
run: |
set -euo pipefail
eval "$CHANGELOG_COMMAND"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: eval on a caller-controlled input.

eval "$CHANGELOG_COMMAND" gives any consumer of this action arbitrary shell execution in the calling job — shell metacharacters, &&/; chains, variable expansion, subshells. This goes beyond the "trust boundary" that SK-422 is meant to enforce.

For a fixed command with optional extra flags, separate the concerns:

Suggested change
eval "$CHANGELOG_COMMAND"
bash -c "$CHANGELOG_COMMAND"

Actually even bash -c has the same injection surface. The cleanest fix is to restrict what this input can do. If the command is always pnpm run <script>, model it that way:

inputs:
  script:
    description: pnpm script name to run (e.g. `changelog:validate`).
    default: changelog:validate
pnpm run "$CHANGELOG_SCRIPT"

If you genuinely need an arbitrary shell command (e.g. npx tool --flag), document that callers are responsible for sanitising the input and add a prominent warning in the action description. The current description gives no indication that the input is shell-executed. Fix this →

set -euo pipefail
# Word-splitting on $MD_GLOBS is intentional — it carries multiple globs.
# shellcheck disable=SC2086
eval "pnpm exec markdownlint-cli2 $MD_GLOBS"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: eval on caller-controlled glob input.

The default value '"**/*.md" "!**/node_modules/**"' smuggles shell quoting into the input so that eval splits it correctly — a strong sign this is the wrong abstraction. Any consumer overriding globs can inject arbitrary shell commands.

mapfile cleanly replaces the eval and keeps the input as plain data:

Suggested change
eval "pnpm exec markdownlint-cli2 $MD_GLOBS"
mapfile -t md_globs <<<"$MD_GLOBS"
pnpm exec markdownlint-cli2 "${md_globs[@]}"

Change the default to newline-separated plain globs (no shell quoting needed):

    default: |
      **/*.md
      !**/node_modules/**

mapfile -t splits on newlines, so each glob becomes a separate array element. No eval, no injection surface, and callers supply clean patterns. Fix this →

with:
node-version-file: ${{ inputs.node-version-file }}
# Restore-only pnpm store cache, keyed on the lockfile hash.
cache: pnpm

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cache: pnpm is not restore-only — it also saves.

The action description, the inline comment ("Restore-only pnpm store cache"), the README catalogue entry, and the CLAUDE.md section all describe this as restore-only. But cache: pnpm on actions/setup-node uses actions/cache under the hood, which registers a post-job step that saves the cache after the job completes. It is not restore-only.

Consequences:

  • The "dedicated save job" contention dance is still avoided at single-package scale (fine), but the stated architecture is wrong.
  • If the estate later adds a dedicated cache-save job or a consumer tries to control save separately, they'll hit unexpected double-save behaviour.

Option A — accept save-too and update every doc string that says "restore-only":
Update description, comment on line 49, README, and CLAUDE.md.

Option B — make it truly restore-only with actions/cache/restore:

    - name: 🟢 Setup Node.js
      uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
      with:
        node-version-file: ${{ inputs.node-version-file }}
        # No cache: here — handled explicitly below.

    - name: ♻️ Restore pnpm store cache
      uses: actions/cache/restore@<sha> # vX.Y.Z
      with:
        path: ~/.local/share/pnpm/store
        key: pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}

Option A is simpler and likely sufficient at single-package scale; Option B matches the documented intent. Either way, the docs and the implementation need to agree. Fix this →

Comment thread .github/actions/shellcheck/action.yml Outdated
Comment on lines +27 to +42
run: |
set -euo pipefail
shellcheck --version
if [ -n "$SC_PATHS" ]; then
# Word-splitting on $SC_PATHS is intentional (multiple paths).
# shellcheck disable=SC2086
files=$(ls -1 $SC_PATHS)
else
# Discover tracked shell scripts; -z/xargs -0 is robust to odd names.
files=$(git ls-files -z '*.sh' '*.bash' | tr '\0' '\n')
fi
if [ -z "$files" ]; then
echo "No shell scripts found to check."
exit 0
fi
echo "$files" | xargs shellcheck --severity="$SEVERITY"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsafe file list building — breaks on filenames with spaces.

Two problems:

  1. ls -1 $SC_PATHS (line 33): ls on a directory input expands to the directory's children, not the directory path itself, and produces unusable output. It also splits on whitespace, so infrastructure scripts would be two arguments.
  2. echo "$files" | xargs shellcheck (line 42): xargs splits on whitespace, which re-breaks any paths that survived to this point.

Build an array instead, matching the pattern used in the git ls-files -z path:

Suggested change
run: |
set -euo pipefail
shellcheck --version
if [ -n "$SC_PATHS" ]; then
# Word-splitting on $SC_PATHS is intentional (multiple paths).
# shellcheck disable=SC2086
files=$(ls -1 $SC_PATHS)
else
# Discover tracked shell scripts; -z/xargs -0 is robust to odd names.
files=$(git ls-files -z '*.sh' '*.bash' | tr '\0' '\n')
fi
if [ -z "$files" ]; then
echo "No shell scripts found to check."
exit 0
fi
echo "$files" | xargs shellcheck --severity="$SEVERITY"
set -euo pipefail
shellcheck --version
files=()
if [ -n "$SC_PATHS" ]; then
# Word-splitting on $SC_PATHS is intentional (space-separated paths).
# shellcheck disable=SC2206
paths=($SC_PATHS)
for p in "${paths[@]}"; do
while IFS= read -r -d '' f; do
files+=("$f")
done < <(find "$p" -type f \( -name '*.sh' -o -name '*.bash' \) -print0 2>/dev/null || true)
done
else
while IFS= read -r -d '' f; do
files+=("$f")
done < <(git ls-files -z '*.sh' '*.bash')
fi
if [ "${#files[@]}" -eq 0 ]; then
echo "No shell scripts found to check."
exit 0
fi
shellcheck --severity="$SEVERITY" "${files[@]}"

set -euo pipefail
base="https://raw.githubusercontent.com/rhysd/actionlint"
url="${base}/${ACTIONLINT_INSTALLER_SHA}/scripts/download-actionlint.bash"
bash <(curl -fsSL "$url") "$ACTIONLINT_VERSION"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remote script execution with a mutable version input.

bash <(curl -fsSL "$url") "$ACTIONLINT_VERSION"

The installer URL is SHA-pinned (good — ACTIONLINT_INSTALLER_SHA is a commit hash, not a tag), and the installer itself checksum-verifies the binary it downloads. However, ACTIONLINT_VERSION comes from an action input and is passed to the installer as the version to fetch. An attacker who can influence that input could request an older, vulnerable actionlint binary.

The risk is low given this is an internal estate action, but worth documenting: the comment above could note that actionlint-version should only be changed via a Dependabot bump, not overridden ad-hoc by consumers. A default + note in the input description ("bump via Dependabot only") would suffice.

Triage of CodeRabbit + Claude review on PR #5:

- lint-markdown / changelog-validate: drop `eval` on caller-supplied inputs
  (shell-injection surface in the SK-422 trust boundary). markdownlint globs are
  now newline-separated plain data read into an array; the changelog validator
  is a `script` input run as `pnpm run <script>` (single argument, never
  reparsed as shell).
- shellcheck: build the target list as an array. The default discovery uses
  NUL-delimited `git ls-files -z` (robust to spaces); `ls`/`echo | xargs`
  removed.
- setup-project: correct the "restore-only" wording in the action, README and
  CLAUDE.md — `cache: pnpm` restores at job start and saves in a post-job step
  (the ADR's intent is avoiding the multi-package dedicated *save job*, which is
  what stays omitted). Behaviour unchanged.
- lint-yaml: note that the yamllint/actionlint version inputs are pinned to
  ci.yml and should be bumped in lockstep, not overridden per consumer (SK-422).

Re-verified with actionlint + yamllint + shellcheck and a full `act` run
(setup-project + lint-yaml + lint-markdown green).

Refs SK-440.
@RobEasthope

Copy link
Copy Markdown
Contributor Author

Triage of review feedback — all addressed in b45ca81

Worked through every CodeRabbit + Claude finding:

Finding Verdict Fix
lint-markdowneval on glob input ✅ valid (injection) Newline-separated plain globs read into an array; eval removed
changelog-validateeval on command input ✅ valid (injection) Replaced with a script input run as pnpm run "$script" (single arg, never reparsed)
shellcheckls/echo | xargs break on spaces & dir inputs ✅ valid Array-based targets; default discovery via NUL-delimited git ls-files -z
setup-projectcache: pnpm is not restore-only ✅ valid (docs wrong) Corrected wording in action/README/CLAUDE.md; kept cache: pnpm (it should save, else the cache never populates — the ADR's "no dedicated save job" intent is unchanged)
lint-yaml — mutable tool-version inputs 🟡 minor Noted the version inputs are pinned to ci.yml and bump in lockstep, not per-consumer

Not changed (deliberately): the eslint/typecheck/test-vitest/test-bats steps pass inputs via unquoted $VAR word-splitting — that's plain argument pass-through, not eval, so it carries no command-injection surface. The bots correctly didn't flag these.

Re-verified: actionlint + yamllint + shellcheck clean; the rewritten shellcheck default-discovery tested under real bash with a space-containing filename; full act run of setup-project + lint-yaml (injected config) + lint-markdown green. CI passing on b45ca81.

`cache: pnpm` restores at job start and saves in a post-job step — it is not
restore-only. The architecture's actual intent is avoiding the multi-package
*dedicated save job*, which stays omitted at single-package scale. Aligns the
ADR with the setup-project action's corrected docs (PR #5).

Refs SK-440.
@RobEasthope
RobEasthope dismissed coderabbitai[bot]’s stale review June 26, 2026 14:35

All findings addressed in b45ca81 (eval removal, shellcheck array-discovery, restore-only doc corrections) and ADR wording in 81e5d34 — see the triage summary comment. Threads are now outdated. Dismissing this stale advisory review to merge.

@RobEasthope
RobEasthope merged commit 132d746 into main Jun 26, 2026
5 checks passed
@RobEasthope
RobEasthope deleted the sk-440-author-the-shared-composite-action-layer-setup-project branch June 26, 2026 14:35
RobEasthope added a commit that referenced this pull request Jun 30, 2026
## What

Reconstructs `shared-workflows`' **entire release history** and
graduates the current code to a stable **`v1.0.0`** (A-585). The repo
publishes no npm package, so a "release" here is a **git tag (`vX.Y.Z`)
+ GitHub release** over the reusable workflows and composite actions.

## Versioning methodology

The merged PR **titles are not reliable** — Conventional-Commit
discipline was loose when the repo was stood up (the GO/NO GO gate
shipped as `ci:`, the versioned ruleset as `chore:`). So every PR was
re-classified by **what it actually changed to the product surface**
(the `reusable-*.yml` workflows + `.github/actions/*` consumers use, and
their reference docs), read from the diff. Pre-1.0 rules: `feature` →
minor, `fix` → patch, breaking → minor (stay in `0.x`), and `1.0.0` is a
deliberate "graduate to stable" milestone.

| Version | PR | True category | Headline |
|---|---|---|---|
| 0.1.0 | #1 | feature | bootstrap: 3 reusable workflows |
| 0.2.0 | #5 | feature | Layer-1 composite-action layer |
| 0.3.0 | #6 | feature | reusable-lint bundle |
| 0.4.0 | #7 | feature | reusable build-test bundle + `build` action |
| 0.4.1 | #8 | fix | repoint build-test pins so the build lane resolves
|
| 0.5.0 | #9 | feature | reusable pkg-release workflow |
| 0.6.0 | #15 | refactor **(breaking)** | rename release → pkg-release |
| 0.7.0 | #16 | **feature** *(titled `ci:`)* | GO/NO GO aggregator gate
+ docs |
| 0.8.0 | #17 | **feature** *(titled `chore:`)* | versioned canonical
estate ruleset |
| 1.0.0 | HEAD | milestone | graduate to stable public surface |

Non-release PRs (self-host CI, ADR, dep bump, SK→A rename, skill
adoptions/re-syncs, check-run rename) are folded into the body of the
release window they merged in — the way a release-please changelog
aggregates.

## Changes

- **`changelog/`** — hand-authored dated entries (one per release) + the
schema doc (`changelog/README.md`), adapted from the estate's
`eslint-config`/`agent-skills` convention.
- **`package.json`** — `0.0.0` → `1.0.0` (stays `private`; the version
is the release anchor, not an npm publish).
- **`reusable-lint.yml`** — pin the five sibling Layer-1 actions to the
**`v0.8.0`** tag (`@7f543be`) instead of the bare pre-release commit,
so Dependabot can track them (`v0.8.0` is the latest backfilled tag that
carries every action and predates this commit, avoiding a self-reference
to `v1.0.0`).
- **`CLAUDE.md` / `README`** — replace the "no release tag exists yet"
wording.

## After merge

The git tags + GitHub releases are cut once this lands:
`v0.1.0`–`v0.8.0` at their historical merge commits, `v1.0.0` at this
PR's squash-merge commit, with notes sourced from each `changelog/`
entry. This unblocks consumers pinning a tag (+ Dependabot) instead of a
moving SHA (A-420 / A-446).

## Deferred (follow-up)

- **release-please automation** + a tag-only release workflow — the
forward release process is a separate decision now the backfill exists.

## Note for review

> Backfilling git tags at historical commits is a deliberate divergence
from the estate precedent (`eslint-config`/`agent-skills` backfilled
changelog *entries* but tagged only going forward) — chosen here because
A-585 explicitly wants the *full tag history* reconstructed.

The pre-existing Prettier drift on the vendored `.claude`/`.agents`
skill bundles is untouched (out of scope).


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Marked the reusable workflows and composite actions as stable at
**v1.0.0**.
  * Updated workflow references to use the newer version tags.

* **Documentation**
  * Clarified release and versioning guidance across the repository.
* Added and expanded changelog documentation and release notes
structure.

* **Chores**
  * Updated internal workflow pins to the latest approved release.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant