feat: migrate-consumers automation for the new-repo migration (Phase 1B)#237
Conversation
1c27687 to
0b861d2
Compare
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
The migration automation is well-structured with good idempotency guards and error handling. The TypeScript CLI correctly handles the documented consumer reference shapes. No high or medium severity bugs found in the changed code.
Implements roadmap issues 10 and 11: - src/rename-consumer-refs/: TypeScript CLI (bundled to dist/rename-consumer-refs.js) that rewrites docker/cagent-action references to docker/docker-agent-action. Handles every consumer reference shape (root action, sub-actions, reusable workflow, gh api URLs), supports rename-only and repin (--sha/--version) modes, and is idempotent. 30 unit tests. - .github/workflows/rename-consumers.yml: workflow_dispatch automation that discovers all org:docker consumers via code search, rewrites every matching workflow file, and opens one signed-commit PR per repo. Dry-run is ON by default and a repos allowlist enables piloting on 2-3 repos before rename day (roadmap issue 11).
- CLI: collect per-file I/O errors instead of aborting mid-loop, and exit 1 when any file failed so callers never commit a partial migration. Logic moved to applyRename() in rename-refs.ts with unit tests (failing file first, remaining files still processed). - Workflow: set -euo pipefail in both script steps — without pipefail a CLI failure was masked by the trailing sed, defeating the skip guard. - Workflow: single EXIT trap registered once before the loop via a cleanup_workdir() helper referencing a global, instead of re-registering a single-quoted trap per iteration. - Workflow: trim allowlist entries with parameter expansion instead of xargs (no backslash/quote interpretation). - Workflow: fix EXISTING_PR detection — jq '.[0].number' prints the literal string "null" when no PR exists, which wrongly took the update-existing-PR branch; use '// empty'.
The code search legitimately matches docker/cagent-action (this workflow's own grep pattern and comments) and the shim repo (its migration instructions reference the old slug). Migrating either would corrupt them, so filter them out of the discovered repo list.
…e-pinning Local simulation against real consumers (docker/agentic-tui) found refs to cagent-action/.github/actions/setup-credentials — a path that no longer exists in the current tree (moved to setup-credentials/). Re-pinning such a ref to a new SHA without rewriting the path would produce a broken uses: reference. The path migration only applies in repin mode: at old SHAs the old path still resolves, so rename-only mode keeps it untouched.
The roadmap changed from an in-place rename (with a deprecation shim and a 30-day sunset) to creating a brand new docker/docker-agent-action repo by copy, with the old repo staying live indefinitely. Adapt the automation accordingly: - Rename rename-consumers.yml -> migrate-consumers.yml (the workflow name specified by roadmap Phase 1B issue 8) and reframe it as incremental, no-deadline consumer migration instead of a one-shot rename-day cutover. - Drop the docker/cagent-action-shim discovery exclusion (no shim anymore); keep excluding the old and new action repos themselves. - Rewrite the consumer PR body: the old repo remains functional, no merge deadline, no shim warning. - Rename src/rename-consumer-refs -> src/migrate-consumer-refs (CLI now bundled as dist/migrate-consumer-refs.js), with matching symbol renames (migrateRefs/applyMigration) and updated docs; branch name becomes auto/migrate-to-docker-agent-action. - Update CHANGELOG.md and AGENTS.md to match.
f4ec759 to
ebef0ce
Compare
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
2 medium-severity findings in the new migrate-consumers.yml workflow.
| persist-credentials: false | ||
|
|
||
| - name: Setup credentials | ||
| uses: ./setup-credentials |
There was a problem hiding this comment.
[MEDIUM] GITHUB_APP_TOKEN masking is not visible in this workflow — relying entirely on setup-credentials to mask it
The workflow sets GH_TOKEN: ${{ env.GITHUB_APP_TOKEN }} in multiple steps (lines 129, 198) after ./setup-credentials runs, but never calls echo "::add-mask::$GITHUB_APP_TOKEN" itself. Per AGENTS.md: "All secret values are masked with ::add-mask:: before any other step can log them."
If setup-credentials does mask the token internally (which it should given team conventions), this is a non-issue. However, this is a newly added workflow file and there is no explicit masking call here, which means:
- If
setup-credentialsis updated in the future and the masking is moved or removed, this workflow silently loses protection. - Any debug/echo of
$GITHUB_APP_TOKENbeforesetup-credentialsruns (e.g., in error paths) would leak it.
Suggested fix: add an explicit mask call after the setup-credentials step completes:
- name: Mask app token
run: echo "::add-mask::$GITHUB_APP_TOKEN"This is belt-and-suspenders and makes the security invariant self-documenting in the workflow file.
- Decouple reviewer assignment from gh pr create: --reviewer fails the entire create when the reviewer is not a collaborator of the consumer repo, silently dropping the migration PR. Reviewer is now added best-effort via gh pr edit after creation, on both paths. - Add underscore to the PLAIN_RE negative lookahead so hypothetical docker/cagent-action_* repos are not rewritten (with tests, including one pinning that .git clone URLs ARE still rewritten). - Explicitly re-mask GITHUB_APP_TOKEN in the workflow: setup-credentials already masks it via core.setSecret, but the belt-and-suspenders call makes the invariant self-documenting and survives refactors.
docker-agent
left a comment
There was a problem hiding this comment.
Assessment: 🟢 APPROVE
The migration automation is well-structured and security-conscious. The TypeScript CLI correctly handles all documented reference shapes, is idempotent, and has solid test coverage (30 tests covering slug-only mode, repin mode, edge cases, and I/O errors). The workflow correctly uses set -euo pipefail, bash arrays to avoid word-splitting, explicit secret masking, and a single EXIT trap for temp-directory cleanup. No high or medium severity bugs were found.
| * 1. prefix — everything before the slug (indentation, `- uses:`, quotes) | ||
| * 2. subpath — optional sub-action / workflow path (e.g. `/review-pr`) | ||
| * 3. ref — the ref after `@` (SHA, tag, or branch) | ||
| * 4. comment — optional trailing ` # vX.Y.Z` comment |
There was a problem hiding this comment.
[LOW] JSDoc comment for USES_RE lists 4 capture groups but the regex has 5
The comment above USES_RE documents groups 1–4 as prefix, subpath, ref, comment, omitting the closeQuote group that sits between ref and comment:
* 1. prefix — everything before the slug
* 2. subpath — optional sub-action / workflow path
* 3. ref — the ref after `@` (SHA, tag, or branch)
* 4. comment — optional trailing ` # vX.Y.Z` comment ← should be group 5
The actual regex captures 5 groups and the destructuring is correct ([, prefix, subpath, ref, closeQuote, comment]), so there is no runtime bug. But a future maintainer reading the JSDoc comment would map closeQuote to "comment" and comment to nothing, which could cause an incorrect edit. Adding 4. closeQuote — optional closing quote (e.g. \"`)` would make the comment accurate.
| * `docker/cagent-action-fork` is not matched. | ||
| */ | ||
| const USES_RE = new RegExp( | ||
| `^(\\s*(?:-\\s*)?uses:\\s*["']?)${escapeRegExp(OLD_SLUG)}((?:/[^@\\s"']*)?)@([^\\s"'#]+)(["']?)([ \\t]*#[^\\n]*)?\\s*$`, |
There was a problem hiding this comment.
[LOW] USES_RE trailing \s*$ silently strips trailing whitespace from rewritten uses: lines
The regex ends with \\s*$, which consumes any trailing spaces or tabs that follow the optional comment on a matched uses: line. Because the reconstructed line (${prefix}${NEW_SLUG}${newSubpath}@${newRef}${closeQuote}${newComment}) does not re-append those characters, any trailing whitespace present in the original line is silently dropped.
In practice YAML linters (and most editors) strip trailing whitespace anyway, so this is unlikely to matter. But the function's contract is "the rest of the file is preserved byte-for-byte" — this is a small hole in that guarantee. Changing the trailing \\s*$ to (\\s*)$ and capturing group 6, then appending it in the rebuild, would make the behaviour fully round-trip preserving.
derekmisler
left a comment
There was a problem hiding this comment.
lgtm, and this is solid work. the rewrite logic is well-structured (pure function + thin I/O wrapper is exactly the right pattern for testability), the test coverage is thorough, and the workflow has a bunch of nice safety properties — bash arrays instead of eval, a single EXIT trap for cleanup, pipefail guarding the node pipe, and the reviewer assignment decoupled from PR creation so a bad collaborator check can't silently drop the PR. left a couple of notes below, but nothing blocking.
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| repository: docker/docker-agent-action | ||
| ref: ${{ github.workflow_sha }} |
There was a problem hiding this comment.
question (non-blocking): just want to flag this in case it's surprising — github.workflow_sha is the commit SHA of the workflow file in the repo where the workflow is running, not the SHA of the thing being built. so when this workflow eventually lives in docker/docker-agent-action, github.workflow_sha will be a SHA in that repo, and this checkout will work correctly.
if you run this from cagent-action today (before the copy), this step would fail because that SHA won't exist in docker/docker-agent-action. that's expected and consistent with the "merging is inert" note in the PR description — just worth knowing why, in case you hit it in testing.
| # previous handler). cleanup_workdir is also called explicitly on | ||
| # every skip/continue path and is a no-op when already cleaned. | ||
| CURRENT_WORK_DIR="" | ||
| cleanup_workdir() { |
There was a problem hiding this comment.
praise (🥇): the single EXIT trap + cleanup_workdir pattern is nicely done. registering a trap per iteration is a subtle footgun (each new trap replaces the previous one), so anchoring it once with a shared $CURRENT_WORK_DIR variable is the right call. the comment explains exactly why too.
| return hasCR ? `${rebuilt}\r` : rebuilt; | ||
| } | ||
|
|
||
| if (PLAIN_RE.test(line)) { |
There was a problem hiding this comment.
nitpick (super non-blocking): the lastIndex reset after .test() is correct (stateful /g regexes in JS are a classic footgun), but if you ever want to simplify this, you can avoid the whole pattern by just not using the /g flag on PLAIN_RE and relying solely on .replace() with /g when you need global replacement. something like:
if (line.includes(OLD_SLUG)) {
otherCount++;
return hasCR ? `${line.replace(PLAIN_RE, NEW_SLUG)}\r` : line.replace(PLAIN_RE, NEW_SLUG);
}the includes() check is faster than a regex .test() for a simple substring guard. but the current approach is also correct, so no action needed.
derekmisler
left a comment
There was a problem hiding this comment.
lgtm, and this is solid work. the rewrite logic is well-structured (pure function + thin I/O wrapper is exactly the right pattern for testability), the test coverage is thorough, and the workflow has a bunch of nice safety properties — bash arrays instead of eval, a single EXIT trap for cleanup, pipefail guarding the node pipe, and the reviewer assignment decoupled from PR creation so a bad collaborator check can't silently drop the PR. left a couple of notes below, but nothing blocking.
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| repository: docker/docker-agent-action | ||
| ref: ${{ github.workflow_sha }} |
There was a problem hiding this comment.
question (non-blocking): just want to flag this in case it's surprising — github.workflow_sha is the commit SHA of the workflow file in the repo where the workflow is running, not the SHA of the thing being built. so when this workflow eventually lives in docker/docker-agent-action, github.workflow_sha will be a SHA in that repo, and this checkout will work correctly.
if you run this from cagent-action today (before the copy), this step would fail because that SHA won't exist in docker/docker-agent-action. that's expected and consistent with the "merging is inert" note in the PR description — just worth knowing why, in case you hit it in testing.
| # previous handler). cleanup_workdir is also called explicitly on | ||
| # every skip/continue path and is a no-op when already cleaned. | ||
| CURRENT_WORK_DIR="" | ||
| cleanup_workdir() { |
There was a problem hiding this comment.
praise (🥇): the single EXIT trap + cleanup_workdir pattern is nicely done. registering a trap per iteration is a subtle footgun (each new trap replaces the previous one), so anchoring it once with a shared $CURRENT_WORK_DIR variable is the right call. the comment explains exactly why too.
| return hasCR ? `${rebuilt}\r` : rebuilt; | ||
| } | ||
|
|
||
| if (PLAIN_RE.test(line)) { |
There was a problem hiding this comment.
nitpick (super non-blocking): the lastIndex reset after .test() is correct (stateful /g regexes in JS are a classic footgun), but if you ever want to simplify this, you can avoid the whole pattern by just not using the /g flag on PLAIN_RE and relying solely on .replace() with /g when you need global replacement. something like:
if (line.includes(OLD_SLUG)) {
otherCount++;
return hasCR ? `${line.replace(PLAIN_RE, NEW_SLUG)}\r` : line.replace(PLAIN_RE, NEW_SLUG);
}the includes() check is faster than a regex .test() for a simple substring guard. but the current approach is also correct, so no action needed.
derekmisler
left a comment
There was a problem hiding this comment.
lgtm, and this is solid work. the rewrite logic is well-structured (pure function + thin I/O wrapper is exactly the right pattern for testability), the test coverage is thorough, and the workflow has a bunch of nice safety properties — bash arrays instead of eval, a single EXIT trap for cleanup, pipefail guarding the node pipe, and the reviewer assignment decoupled from PR creation so a bad collaborator check can't silently drop the PR. left a couple of notes below, but nothing blocking.
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| repository: docker/docker-agent-action | ||
| ref: ${{ github.workflow_sha }} |
There was a problem hiding this comment.
question (non-blocking): just want to flag this in case it's surprising — github.workflow_sha is the commit SHA of the workflow file in the repo where the workflow is running, not the SHA of the thing being built. so when this workflow eventually lives in docker/docker-agent-action, github.workflow_sha will be a SHA in that repo, and this checkout will work correctly.
if you run this from cagent-action today (before the copy), this step would fail because that SHA won't exist in docker/docker-agent-action. that's expected and consistent with the "merging is inert" note in the PR description — just worth knowing why, in case you hit it in testing.
| # previous handler). cleanup_workdir is also called explicitly on | ||
| # every skip/continue path and is a no-op when already cleaned. | ||
| CURRENT_WORK_DIR="" | ||
| cleanup_workdir() { |
There was a problem hiding this comment.
praise (🥇): the single EXIT trap + cleanup_workdir pattern is nicely done. registering a trap per iteration is a subtle footgun (each new trap replaces the previous one), so anchoring it once with a shared $CURRENT_WORK_DIR variable is the right call. the comment explains exactly why too.
| return hasCR ? `${rebuilt}\r` : rebuilt; | ||
| } | ||
|
|
||
| if (PLAIN_RE.test(line)) { |
There was a problem hiding this comment.
nitpick (super non-blocking): the lastIndex reset after .test() is correct (stateful /g regexes in JS are a classic footgun), but if you ever want to simplify this, you can avoid the whole pattern by just not using the /g flag on PLAIN_RE and relying solely on .replace() with /g when you need global replacement. something like:
if (line.includes(OLD_SLUG)) {
otherCount++;
return hasCR ? `${line.replace(PLAIN_RE, NEW_SLUG)}\r` : line.replace(PLAIN_RE, NEW_SLUG);
}the includes() check is faster than a regex .test() for a simple substring guard. but the current approach is also correct, so no action needed.
derekmisler
left a comment
There was a problem hiding this comment.
lgtm, and this is solid work. the rewrite logic is well-structured (pure function + thin I/O wrapper is exactly the right pattern for testability), the test coverage is thorough, and the workflow has a bunch of nice safety properties — bash arrays instead of eval, a single EXIT trap for cleanup, pipefail guarding the node pipe, and the reviewer assignment decoupled from PR creation so a bad collaborator check can't silently drop the PR. left a couple of notes below, but nothing blocking.
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| repository: docker/docker-agent-action | ||
| ref: ${{ github.workflow_sha }} |
There was a problem hiding this comment.
question (non-blocking): just want to flag this in case it's surprising — github.workflow_sha is the commit SHA of the workflow file in the repo where the workflow is running, not the SHA of the thing being built. so when this workflow eventually lives in docker/docker-agent-action, github.workflow_sha will be a SHA in that repo, and this checkout will work correctly.
if you run this from cagent-action today (before the copy), this step would fail because that SHA won't exist in docker/docker-agent-action. that's expected and consistent with the "merging is inert" note in the PR description — just worth knowing why, in case you hit it in testing.
| # previous handler). cleanup_workdir is also called explicitly on | ||
| # every skip/continue path and is a no-op when already cleaned. | ||
| CURRENT_WORK_DIR="" | ||
| cleanup_workdir() { |
There was a problem hiding this comment.
praise (🥇): the single EXIT trap + cleanup_workdir pattern is nicely done. registering a trap per iteration is a subtle footgun (each new trap replaces the previous one), so anchoring it once with a shared $CURRENT_WORK_DIR variable is the right call. the comment explains exactly why too.
| return hasCR ? `${rebuilt}\r` : rebuilt; | ||
| } | ||
|
|
||
| if (PLAIN_RE.test(line)) { |
There was a problem hiding this comment.
nitpick (super non-blocking): the lastIndex reset after .test() is correct (stateful /g regexes in JS are a classic footgun), but if you ever want to simplify this, you can avoid the whole pattern by just not using the /g flag on PLAIN_RE and relying solely on .replace() with /g when you need global replacement. something like:
if (line.includes(OLD_SLUG)) {
otherCount++;
return hasCR ? `${line.replace(PLAIN_RE, NEW_SLUG)}\r` : line.replace(PLAIN_RE, NEW_SLUG);
}the includes() check is faster than a regex .test() for a simple substring guard. but the current approach is also correct, so no action needed.
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| repository: docker/docker-agent-action | ||
| ref: ${{ github.workflow_sha }} |
There was a problem hiding this comment.
issue: github.workflow_sha is the commit SHA of the workflow file in the repo where the workflow is running — which right now is cagent-action. but line 108 points actions/checkout at docker/docker-agent-action, so it's asking that repo to check out a SHA that only exists in cagent-action. that'll fail.
the intent here is to check out the current source so the CLI tools can be built. since the source lives in cagent-action for now, the fix is to just let the checkout default to the current repo:
- name: Checkout source for build
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.sha }}
persist-credentials: falsedrop the repository: line entirely so it defaults to whatever repo the workflow is running in. once the content is copied to docker/docker-agent-action, this will work correctly there too — github.sha will then be a SHA in that repo.
i found a bug after originally approving
- Checkout source for build: drop the hardcoded repository + workflow_sha pair (that SHA only exists in the repo the workflow runs from, so the cross-repo checkout would fail). Default to the current repo at github.sha, which works both before and after the content is copied to docker/docker-agent-action. - migrate-refs: replace the stateful PLAIN_RE.test() + lastIndex reset with a plain includes() guard followed by replace(); compare before/ after so lookahead-excluded near-misses (e.g. docker/cagent-action-fork) don't inflate otherCount.
derekmisler
left a comment
There was a problem hiding this comment.
both fixes look good. the checkout now defaults to the current repo at github.sha (no more cross-repo SHA mismatch), and the includes() guard + compare-before/after approach for otherCount is cleaner than the stateful lastIndex dance. ci is all green. approving on the new commit.
Summary
Implements roadmap Phase 1B (issues 8 and 9): consumer migration automation for the move from
docker/cagent-actionto the newdocker/docker-agent-actionrepo.src/migrate-consumer-refs/— TypeScript CLI (bundled todist/migrate-consumer-refs.js) rewritingdocker/cagent-action→docker/docker-agent-action. Handles every consumer reference shape (root action, sub-actions, legacy.github/actions/setup-credentialspath, reusable workflow,gh apiURLs), supports slug-only and repin (--sha/--version) modes, idempotent. 30 unit tests..github/workflows/migrate-consumers.yml(roadmap issue 8) —workflow_dispatch-only automation: discovers allorg:dockerconsumers via code search, rewrites every matching workflow file, opens one signed-commit PR per repo. Migration PRs explicitly tell consumers there is no deadline since the old repo remains functional.dry-runinput (default ON) prints the would-be diffs without committing;reposallowlist enables piloting on 2–3 repos per tier before broad rollout.Backward compatibility of the old repo (roadmap issue 10) requires no code: this repo simply stays published and maintained — no shim is built.
Merging is inert: the workflow has no automatic trigger, and a manual dispatch would currently fail fast at version resolution until
docker/docker-agent-actionpublishes its first release.Test plan
pnpm test— 419 tests pass (30 new)setup-credentialspath repin)docker/docker-agent-actionhas its first release:gh workflow run migrate-consumers.yml -f dry-run=true -f repos=docker/<pilot-repo>