Skip to content

feat: migrate action to composite workflow#117

Merged
maxisam merged 17 commits into
mainfrom
feat/composite-action
Jun 26, 2026
Merged

feat: migrate action to composite workflow#117
maxisam merged 17 commits into
mainfrom
feat/composite-action

Conversation

@maxisam

@maxisam maxisam commented Jun 26, 2026

Copy link
Copy Markdown
Owner

Summary

  • Replaces the bundled TypeScript action with a composite action that orchestrates shell steps and focused Node helper scripts.
  • Moves report generation, config loading, argument construction, and merge/comment handling into ESM scripts with focused tests.
  • Updates CI, package metadata, migration notes, README, and walkthrough docs for the composite-action architecture.

Impact

Consumers keep using the same action entrypoint, but the repository no longer depends on a bundled dist/index.js runtime. The action now runs through action.yml composite steps and helper scripts committed in scripts/.

Validation

  • pnpm install --frozen-lockfile
  • pnpm run format-check
  • pnpm test
  • pnpm all
  • git diff --check origin/main...HEAD
  • Parsed action.yml with js-yaml
  • Focused /home/saml/.dotnet/dotnet format --verify-no-changes check against __tests__/dotnet/Console generated plan

GitHub-hosted end-to-end action execution remains the release gate after PR creation.

maxisam added 16 commits June 26, 2026 09:39
Phase 0: document the plan to replace the bundled ncc/dist JS action with
a composite action (shell + actions/github-script + tested scripts/*.mjs
helpers), dropping the build step and committed bundle.

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
Add scripts/merge.mjs (deepMerge + dedupe, replacing the `deepmerge` dep) and
scripts/read-config.mjs (3-way JSON/YAML config merge, ported 1:1 from
src/readConfig.ts). YAML parsing is injected so the module stays dependency-free
at action runtime. Covered by node:test (merge + read-config), porting the old
readConfig coverage.

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
Add scripts/format-args.mjs: buildArgs, generateFormatCommandArgs, isEnabledBlock
(from src/dotnet.ts) plus buildDefaultOptions, finalizeEnabled, checkIsDryRun and a
planFormat convenience (from src/format.ts getOptions). Pure/dependency-free, emits
ready-to-run `dotnet` argv arrays so the shell runner never quotes --include lists.
Empty workspace now throws (caller maps to core.setFailed). Ported dotnet.test.ts
coverage and added cases for the new orchestration functions.

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
Add scripts/report-common.mjs (shared footer), scripts/dotnet-report.mjs
(getReportFiles/getReportHeader/generateReport from src/dotnet.ts) and
scripts/jscpd-report.mjs (message building, isOverThreshold, buildAnnotations
from src/duplicated.ts). GitHub context is injected as a plain object instead of
read from @actions/github, keeping the helpers pure. Covered by node:test.

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
The branch shipped merge.mjs/read-config.mjs/format-args.mjs (not the originally
planned resolve-config.mjs/build-format-args.mjs). Update the step list and task
list to match and mark Phase 1 done.

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
Switch runs.using node24/dist -> composite. Keep all inputs/outputs identical;
wire outputs.hasChanges/hasDuplicates to step ids `format`/`jscpd`. Final steps:
dotnet env setup, problem-matcher add/remove via ::add-matcher::/::remove-matcher::
(problem-matcher.json copied to action root), and both upload-artifact steps.
The format/jscpd/commit/failFast logic steps are placeholders wired with correct
ids, conditionals and env passing for Phases 3-5.

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
Refactor resolveConfig to delegate all file I/O + parsing to an injected
loadObject(absPath) (async-capable), keeping a single merge-precedence code path
shared by tests and runtime; add resolveConfigPaths. New scripts/steps/resolve-config.mjs
builds default options from env inputs, 3-way merges config (JSON natively, YAML via
`npx -y js-yaml`), folds in the PR changed-files lookup, and writes the normalized
run plan (commands + isDryRun) to $RUNNER_TEMP/df-config.json. Tests updated to the
loader signature (33 pass).

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
scripts/steps/comment.mjs ports getExistingCommentId/comment/updateComment from
git.ts (match a prior bot comment by header, update-or-create). scripts/steps/
format-report.mjs reads the report files, builds markdown via dotnet-report.mjs,
writes the job summary, and upserts the PR comment on pull_request events.

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
…action.yml

Replace Phase 3 placeholders with real steps:
- resolve-config: github-script calling scripts/steps/resolve-config.mjs (env-passed inputs)
- dotnet-format (shell): optional nuget restore, loop the planned argv arrays via
  jq+readarray (preserves space-joined --include as one arg), detect Format complete /
  Unable to fix, emit formatResult output
- upload-artifact before report removal
- format-report: github-script calling scripts/steps/format-report.mjs
- commit/push (shell, id: format): remove report dir, dry-run short-circuit, hasChanges
  from git status, git config + commit + push with rebase-retry on PR events
- failFast gate now consumes steps.dotnet-format.outputs.formatResult

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
Address codex review:

High: the format runner forced dotnet to succeed with `|| true` and only recorded
formatResult, so a missing SDK / invalid workspace / crash could pass when
failFast=false. Now capture dotnet's exit code: a non-zero exit with no "Format
complete" marker is treated as an execution failure and exits 1 unconditionally
(independent of failFast). Formatting findings — which still end stdout with
"Format complete" — remain gated by the failFast step, preserving parity. dotnet
restore failures now also fail the step (the original threw on restore failure).

Medium: the commit step used `set -uo pipefail` (no -e), so a failed
fetch/stash/checkout/add/commit fell through to the push loop and could report
success. Switch to `set -euo pipefail`; the push loop's expected non-zero push is
already guarded with `set +e`/`set -e`.

Verified: bash -n on all run scripts; stubbed-dotnet test shows success/finding
exit 0 and infra failure exits 1.

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
Factor the JSON-native / YAML-via-`npx js-yaml` loadObject out of resolve-config.mjs
into scripts/steps/load-config.mjs so the dotnet format and jscpd resolve paths
share one implementation (no divergence risk).

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
jscpd-run (shell): resolve scanPath (file->dir), output dir, config-file candidate
(scanPath then cwd), prefer installed jscpd/cpd else npx --yes jscpd@5, run with
json,markdown,console-full reporters; tolerate jscpd's non-zero over-threshold exit.

scripts/steps/jscpd-report.mjs (github-script): read jscpd-report.json, compute the
merged threshold via resolveConfig, build the message with jscpd-report.mjs, write the
job summary + PR comment, overwrite the markdown artifact with the full message, emit
annotations, set hasDuplicates to the over-threshold result, and setFailed when
jscpdCheckAsError && over threshold. Ported 1:1 from duplicatedCheck.

Verified with a stubbed report dir: over+asError -> setFailed + hasDuplicates=true;
over+warnOnly -> hasDuplicates=true no fail; under-threshold -> hasDuplicates=false;
no-duplicates -> false. 33 unit tests green, biome clean, bash -n clean.

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
Address codex review: the composite flow wrote the jscpd report to the workspace,
uploaded it, then left it behind — unlike the original which io.rmRF(outputDir) on
every path. Since the artifact upload is now its own later step, add a cleanup step
after it that runs always() (even when the report step failed the build via
jscpdCheckAsError) to remove the report dir, with guards against deleting the
workspace root. Restores parity with src/duplicated.ts and prevents stale reports on
repeat runs / downstream git-status pollution.

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
The composite action no longer needs a bundle or TypeScript source:
- remove dist/ (the ~11M committed ncc bundle) and src/ (ported to scripts/*.mjs)
- remove scripts/finalize-dist.mjs and the old *.test.ts (coverage ported to *.test.mjs)
- delete tsconfig.json; drop TypeScript entirely
- package.json: remove @actions/*, @octokit/rest, @octokit/types, deepmerge, ncc,
  typescript, @types/*; js-yaml moves to devDependencies (tests only; runtime uses
  `npx -y js-yaml`). Scripts reduce to biome + node:test (no build/package/copy-matcher).
- biome.json: drop the now-defunct dist/lib excludes
- regenerate pnpm-lock.yaml

`pnpm all` (biome check + 33 node:test) is green.

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
CI no longer builds or bundles, so replace the build/pnpm-all/dist-diff/auto-commit
job with a single Lint & Test job (pnpm install -> biome check -> node:test). Remove
the dist/** paths-ignore and the checkout ref/auto-push that the dist-commit needed.

Claude-Session: https://claude.ai/code/session_01JxEe85CyGm5rCeoLFdUZed
@maxisam maxisam marked this pull request as ready for review June 26, 2026 17:50
@maxisam maxisam changed the title [codex] migrate action to composite workflow feat: migrate action to composite workflow Jun 26, 2026
@maxisam maxisam merged commit 5c6c7ef into main Jun 26, 2026
2 of 3 checks passed
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