feat: migrate action to composite workflow#117
Merged
Conversation
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
This was referenced Jun 27, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Impact
Consumers keep using the same action entrypoint, but the repository no longer depends on a bundled
dist/index.jsruntime. The action now runs throughaction.ymlcomposite steps and helper scripts committed inscripts/.Validation
pnpm install --frozen-lockfilepnpm run format-checkpnpm testpnpm allgit diff --check origin/main...HEADaction.ymlwithjs-yaml/home/saml/.dotnet/dotnet format --verify-no-changescheck against__tests__/dotnet/Consolegenerated planGitHub-hosted end-to-end action execution remains the release gate after PR creation.