Conversation
…n (apm_pack.cjs)
- Add actions/setup/js/apm_pack.cjs: JS implementation of apm pack --archive
- Mirrors packer.py: reads apm.yml + apm.lock.yaml, filters by target with
cross-target mapping (.github/skills/ → .claude/skills/ etc.), copies files,
writes enriched apm.lock.yaml with pack: header, creates .tar.gz via tar
- Imports parseAPMLockfile/unquoteYaml from apm_unpack.cjs (no duplication)
- Full security: path-traversal rejection, symlink skipping, boundary checks
- Emits bundle-path output via core.setOutput
- ENV: APM_WORKSPACE, APM_BUNDLE_OUTPUT, APM_TARGET
- Add actions/setup/js/run_apm_pack.cjs: standalone entry point for CI/local
- Add actions/setup/js/apm_pack.test.cjs: 57 vitest tests covering
parseApmYml, detectTarget, filterFilesByTarget (all target/cross-target combos),
scalarToYaml, serializeLockfileYaml, security checks, packBundle integration
(real filesystem), main() smoke test
- Update pkg/workflow/apm_dependencies.go: replace GenerateAPMPackStep to
emit two steps instead of microsoft/apm-action:
1. Shell step: pip install apm-cli + apm install (uses GITHUB_APM_PAT)
2. github-script step (id: apm_pack): runs apm_pack.cjs, emits bundle-path
- Update pkg/workflow/apm_dependencies_test.go: update all assertions for
new two-step structure (GITHUB_APM_PAT, apm_pack.cjs, no microsoft/apm-action)
- Extend .github/workflows/ci.yml js-apm-unpack-integration job: test all
4 pack/unpack combinations (py+py, py+js, js+py, js+js) against reference
- Run make recompile: update smoke-claude.lock.yml and golden files
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f98a6774-0a9e-4331-9403-daf90b81eeeb
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…t printf format string issues Package names containing '%' characters would be misinterpreted as printf format specifiers. Switch to 'echo' which passes the string verbatim. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f98a6774-0a9e-4331-9403-daf90b81eeeb Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
@copilot create a daily agentic workflow using Claude that watches the Microsoft/apm repo and ensures that updates to Microsoft/apm are mirrored in the JavaScript implementation. Also add ci integration tests if needed. Use cache memory to track updates. |
…ith microsoft/APM Adds a daily Claude workflow that: - Monitors microsoft/APM repo (packer.py, unpacker.py, lockfile_enrichment.py) for upstream changes using commit SHAs via GitHub API - Uses cache-memory (/tmp/gh-aw/cache-memory/apm-js-mirror/) to track last-seen SHAs and avoid redundant API calls when nothing has changed - Compares TARGET_PREFIXES, CROSS_TARGET_MAPS, algorithm steps, LockedDependency fields, and lockfile YAML format between Python source and JS files - Creates a PR with JS fixes when functional differences are found - Runs vitest tests after each update to validate correctness - Creates an issue for changes too complex to auto-fix - Exits with noop when JS files are already in sync Agent-Logs-Url: https://github.com/github/gh-aw/sessions/af0c4bd6-f73a-424e-b0b3-4dad8300ffa2 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in commit 2fa9cec. Added
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
There was a problem hiding this comment.
Pull request overview
This PR replaces the microsoft/apm-action pack step with an in-repo JavaScript implementation (apm_pack.cjs) and adds CI + an agentic workflow to keep the JS pack/unpack logic aligned with upstream microsoft/APM.
Changes:
- Add
actions/setup/js/apm_pack.cjs+run_apm_pack.cjsand a new vitest suite for pack behavior. - Update workflow generation to install
apm-clivia pip and run pack viaactions/github-script, emittingbundle-path. - Expand CI to validate Python/JS pack+unpack compatibility and add an upstream mirror-monitor workflow (md + compiled lock).
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
actions/setup/js/apm_pack.cjs |
New JS implementation of apm pack --archive, including target filtering, safety checks, lockfile enrichment, and tar creation. |
actions/setup/js/run_apm_pack.cjs |
Standalone runner for CI/local usage with minimal @actions/* shims. |
actions/setup/js/apm_pack.test.cjs |
New vitest coverage for packer behavior, mapping rules, YAML serialization, and safety checks. |
pkg/workflow/apm_dependencies.go |
Switch pack generation from a single microsoft/apm-action step to pip-install + github-script pack step. |
pkg/workflow/apm_dependencies_test.go |
Update assertions to match the new two-step pack implementation and env var changes. |
.github/workflows/ci.yml |
Add JS pack step and compare outputs across Python/JS pack/unpack combinations. |
.github/workflows/smoke-claude.lock.yml |
Recompiled workflow reflecting new pack steps and env usage. |
.github/workflows/apm-js-mirror-monitor.md |
New daily monitor workflow definition to track upstream Python changes and keep JS in sync. |
.github/workflows/apm-js-mirror-monitor.lock.yml |
Compiled lock workflow for the mirror monitor. |
pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/basic-copilot.golden |
Golden output refresh after workflow recompilation. |
pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/smoke-copilot.golden |
Golden output refresh after workflow recompilation. |
pkg/workflow/testdata/wasm_golden/TestWasmGolden_CompileFixtures/with-imports.golden |
Golden output refresh after workflow recompilation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // 3. Parse lockfile | ||
| const lockfile = parseAPMLockfile(lockfileContent); | ||
| core.info(`[APM Pack] Lockfile version : ${lockfile.lockfile_version}`); |
There was a problem hiding this comment.
Packing rewrites apm.lock.yaml from the parsed object returned by parseAPMLockfile(), but that parser currently ignores unknown per-dependency fields (e.g., is_local in .github/workflows/ci.yml's test lockfile). As a result, the enriched lockfile written into the bundle can silently drop fields present in the original lockfile, which can break compatibility as the lockfile schema evolves. Consider preserving unknown dependency keys during parsing/serialization (or explicitly supporting fields like is_local) so pack is non-destructive aside from the intended pack: enrichment and deployed_files filtering.
|
|
||
| - **Repository**: ${{ github.repository }} | ||
| - **Run**: ${{ github.run_id }} | ||
| - **Date**: ${{ github.run_id }} |
There was a problem hiding this comment.
The run header lists a Date field but uses ${{ github.run_id }} (and duplicates the run id). This makes the rendered context misleading for the agent. Consider changing this to ${{ github.run_started_at }} (or remove the Date line) and keep run_id on its own line.
| - **Date**: ${{ github.run_id }} | |
| - **Date**: ${{ github.run_started_at }} |
actions/setup/js/apm_pack.test.cjs
Outdated
| it("returns empty array when no files match target", () => { | ||
| const files = [".github/skills/foo/"]; | ||
| const { files: result } = filterFilesByTarget(files, "claude"); | ||
| // No direct matches, but .github/skills/ → .claude/skills/ cross-map applies | ||
| expect(result).toContain(".claude/skills/foo/"); | ||
| }); |
There was a problem hiding this comment.
This test name is misleading: it says "returns empty array when no files match target", but the assertions expect a cross-target mapping result to be present (i.e., non-empty). Renaming it to reflect the cross-map behavior would make the intent clearer and prevent confusion when debugging failures.
pkg/workflow/apm_dependencies.go
Outdated
| " printf 'name: gh-aw-workspace\\nversion: 0.0.0\\ndependencies:\\n apm:\\n' > apm.yml", | ||
| ) | ||
|
|
||
| for _, dep := range apmDeps.Packages { | ||
| lines = append(lines, " - "+dep) | ||
| // Use echo instead of printf to avoid printf interpreting dep as a format string | ||
| // (package names with '%' characters would cause unexpected behavior with printf). | ||
| lines = append(lines, fmt.Sprintf(" echo ' - %s' >> apm.yml", dep)) | ||
| } |
There was a problem hiding this comment.
The generated shell script appends dependency slugs using echo ' - %s' >> apm.yml. Because dep is interpolated into a single-quoted shell string without escaping/validation, a malicious package value containing a single quote or shell metacharacters could break out of the quotes and inject arbitrary commands into the workflow run. Please either validate apmDeps.Packages against a strict allowlist (e.g., ^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$) before generating steps, or generate apm.yml without embedding untrusted data inside shell-quoted strings (e.g., write the full YAML via a heredoc or generate it via a small script that treats package names as data).
See below for a potential fix:
" cat <<'APM_YAML' > apm.yml",
" name: gh-aw-workspace",
" version: 0.0.0",
" dependencies:",
" apm:",
)
for _, dep := range apmDeps.Packages {
// Each dependency is written as a literal line in the heredoc; the quoted
// delimiter ('APM_YAML') ensures the shell does not interpret any characters
// in dep (no parameter expansion, command substitution, etc.).
lines = append(lines, fmt.Sprintf(" - %s", dep))
}
lines = append(lines,
" APM_YAML",
" apm install",
)
actions/setup/js/apm_pack.cjs
Outdated
| * Maps bundle_path_prefix → disk_path_prefix for a given target. | ||
| * Only skills/ and agents/ are semantically identical across targets. |
There was a problem hiding this comment.
The JSDoc for CROSS_TARGET_MAPS says it maps bundle_path_prefix → disk_path_prefix, but the implementation applies the map as srcPrefix (input/deployed_files path) → dstPrefix (bundle path). This mismatch makes the mapping semantics hard to reason about and risks future mistakes when updating the table. Please update the comment/type description to reflect the actual direction used by filterFilesByTarget().
| * Maps bundle_path_prefix → disk_path_prefix for a given target. | |
| * Only skills/ and agents/ are semantically identical across targets. | |
| * Maps srcPrefix (disk/deployed_files path) → dstPrefix (bundle path) for a given target, | |
| * as used by filterFilesByTarget(). Only skills/ and agents/ are semantically identical | |
| * across targets. |
actions/setup/js/apm_pack.cjs
Outdated
| const hasGitHub = fs.existsSync(path.join(workspaceDir, ".github")); | ||
| const hasClaude = fs.existsSync(path.join(workspaceDir, ".claude")); |
There was a problem hiding this comment.
detectTarget() checks .github and .claude with existsSync() but does not verify they are directories (unlike the .cursor / .opencode checks). If a file named .github/.claude exists, this will mis-detect the target and can later cause confusing failures. Consider using lstatSync(...).isDirectory() consistently for all target folders (with a safe try/catch or existsSync guard).
| const hasGitHub = fs.existsSync(path.join(workspaceDir, ".github")); | |
| const hasClaude = fs.existsSync(path.join(workspaceDir, ".claude")); | |
| const hasGitHub = fs.existsSync(path.join(workspaceDir, ".github")) && fs.lstatSync(path.join(workspaceDir, ".github")).isDirectory(); | |
| const hasClaude = fs.existsSync(path.join(workspaceDir, ".claude")) && fs.lstatSync(path.join(workspaceDir, ".claude")).isDirectory(); |
actions/setup/js/apm_pack.cjs
Outdated
| lines.push(`- repo_url: ${dep.repo_url}`); | ||
| if (dep.host !== null) lines.push(` host: ${dep.host}`); | ||
| else lines.push(` host: null`); | ||
| if (dep.resolved_commit !== null) lines.push(` resolved_commit: ${dep.resolved_commit}`); | ||
| else lines.push(` resolved_commit: null`); | ||
| if (dep.resolved_ref !== null) lines.push(` resolved_ref: ${dep.resolved_ref}`); | ||
| else lines.push(` resolved_ref: null`); | ||
| if (dep.version !== null) lines.push(` version: ${scalarToYaml(dep.version)}`); | ||
| else lines.push(` version: null`); | ||
| if (dep.virtual_path !== null) lines.push(` virtual_path: ${dep.virtual_path}`); | ||
| else lines.push(` virtual_path: null`); | ||
| lines.push(` is_virtual: ${dep.is_virtual ? "true" : "false"}`); | ||
| lines.push(` depth: ${dep.depth}`); | ||
| if (dep.resolved_by !== null) lines.push(` resolved_by: ${dep.resolved_by}`); | ||
| else lines.push(` resolved_by: null`); | ||
| if (dep.package_type !== null) lines.push(` package_type: ${dep.package_type}`); | ||
| else lines.push(` package_type: null`); | ||
| if (dep.source !== null) lines.push(` source: ${dep.source}`); | ||
| else lines.push(` source: null`); | ||
| if (dep.local_path !== null) lines.push(` local_path: ${dep.local_path}`); | ||
| else lines.push(` local_path: null`); | ||
| if (dep.content_hash !== null) lines.push(` content_hash: ${dep.content_hash}`); | ||
| else lines.push(` content_hash: null`); |
There was a problem hiding this comment.
serializeLockfileYaml() claims to match PyYAML safe_dump quoting, but several scalar fields are emitted without scalarToYaml() (e.g., host, resolved_commit, resolved_ref, virtual_path, resolved_by, etc.). Values like on, off, yes, no, numeric-looking refs, or ISO timestamps can be re-interpreted by YAML parsers as booleans/numbers/datetimes, changing types after a round-trip. To keep parity with PyYAML and avoid schema drift, run all scalar string fields through scalarToYaml() (and consider doing the same for packMeta.format/target).
|
@copilot review comments |
- CROSS_TARGET_MAPS JSDoc: fix direction comment (src→dst, not bundle→disk)
- detectTarget(): use lstatSync().isDirectory() for .github/.claude consistently
with .cursor/.opencode checks to reject plain files with those names
- serializeLockfileYaml(): run all string scalar fields (repo_url, host,
resolved_commit, resolved_ref, virtual_path, resolved_by, package_type,
source, local_path, content_hash, deployed_files items, packMeta.format/target)
through scalarToYaml() to prevent YAML keyword/number misinterpretation
- LockedDependency.extra: add extra:{} field to preserve unknown dep fields
(e.g. is_local) during parse/serialize round-trip so pack is non-destructive
- test rename: 'returns empty array when no files match target' →
'applies cross-target mapping when no direct-match files exist'
- apm_dependencies.go: replace echo with quoted heredoc (<<'APM_YAML') to
prevent shell injection from package names containing single quotes/metacharacters
- apm-js-mirror-monitor.md: remove duplicate Date line that echoed run_id twice
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b34de4d8-a161-4feb-a73d-a5e163933aa7
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…ion in detectTarget() Agent-Logs-Url: https://github.com/github/gh-aw/sessions/b34de4d8-a161-4feb-a73d-a5e163933aa7 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
All 7 review issues addressed in commit 7442147 (+ a follow-up path-variable cleanup in d391154):
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot remove all depends to apm-cli. Reimplement in JavaScript to be self contained NOT ALLOWED:
|
There was a problem hiding this comment.
💥 Automated smoke test review - all systems nominal!
💥 [THE END] — Illustrated by Smoke Claude
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| issues: read |
There was a problem hiding this comment.
Smoke test review comment — The workflow description is clear. Consider adding a brief note in the frontmatter about what cache-memory path is used so maintainers can quickly identify the state storage location without reading the full body.
|
|
||
| // 3. Parse lockfile | ||
| const lockfile = parseAPMLockfile(lockfileContent); | ||
| core.info(`[APM Pack] Lockfile version : ${lockfile.lockfile_version}`); |
There was a problem hiding this comment.
Smoke test review comment — As noted in the existing review, consider preserving unknown lockfile fields during serialization to avoid silent field drops during pack operations. A spread operator or Object.assign pattern on the dependency object before overriding deployed_files would handle this gracefully.
There was a problem hiding this comment.
Agreed — preserving unknown lockfile fields during serialization is a solid defensive pattern. Using spread ({ ...existingDep, deployed_files: filtered }) would handle this elegantly without needing explicit field enumeration. 🤖
📰 BREAKING: Report filed by Smoke Copilot
Replaces the two-step approach (shell `pip install apm-cli` + `apm install`, then github-script `apm_pack.cjs`) with a single self-contained github-script step that runs both install and pack using pure JavaScript. New files: - actions/setup/js/apm_install.cjs — downloads APM packages from GitHub using the REST API (GITHUB_APM_PAT via @actions/github Octokit), writes deployed files to workspace, writes apm.yml + apm.lock.yaml - actions/setup/js/run_apm_install.cjs — standalone runner for CI / local dev - actions/setup/js/apm_install.test.cjs — 28 unit tests with mocked Octokit Updated: - pkg/workflow/apm_dependencies.go — GenerateAPMPackStep emits a single github-script step; packages passed as JSON via APM_PACKAGES env var (single-quoted in YAML to avoid array-literal parsing) - pkg/workflow/apm_dependencies_test.go — assertions updated for new step - .github/workflows/ci.yml — APM integration job uses JS-only round-trip (no Python, no pip install); renamed to js-apm-integration - .github/workflows/smoke-claude.lock.yml — recompiled Agent-Logs-Url: https://github.com/github/gh-aw/sessions/2645f721-40b5-482e-8768-5b57590b4470 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in commit 58732d2. The
Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
Agent Container Tool Check
Result: 12/12 tools available ✅ Overall Status: PASS
|
|
@copilot create a test suite for apm support and a makefile target "test-apm" Run test-apm and fix javascript until the test passes |
|
✅ GitHub MCP | ✅ MCP Scripts GH CLI | ✅ Playwright | ✅ Web Fetch | ✅ File Write | ✅ Bash | ✅ Discussion | ✅ Build | ✅ PR Review | ✅ Dispatch Overall: PASS 🎉 cc
|
There was a problem hiding this comment.
Smoke test review for PR #23440. Two inline comments added: one on the workflow frontmatter suggesting a timeout-minutes guard, and one on apm_pack.cjs recommending round-trip unit tests for YAML scalar quoting edge cases.
📰 BREAKING: Report filed by Smoke Copilot
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| issues: read |
There was a problem hiding this comment.
Smoke test review comment — The workflow description is clear and well-structured. One suggestion: add an explicit timeout-minutes for the monitoring job to prevent runaway runs if the APM registry becomes unresponsive.
|
|
||
| // 3. Parse lockfile | ||
| const lockfile = parseAPMLockfile(lockfileContent); | ||
| core.info(`[APM Pack] Lockfile version : ${lockfile.lockfile_version}`); |
There was a problem hiding this comment.
Smoke test review comment — Consider adding explicit unit tests for the serializeLockfileYaml() round-trip behavior with YAML boolean-like values (yes, no, on, off) to prevent regressions as the lockfile schema evolves.
|
📰 VERDICT: Smoke Copilot has concluded. All systems operational. This is a developing story. 🎤 |
…for microsoft/apm-sample-package Agent-Logs-Url: https://github.com/github/gh-aw/sessions/f0a89f89-af57-4237-8ec6-d8a322d680c3 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
|
Smoke Test Run 23711884225 — Core: ✅✅✅✅✅✅✅✅✅✅✅ | PR Review: ✅✅✅
|
There was a problem hiding this comment.
💥 Automated smoke test review - all systems nominal!
💥 [THE END] — Illustrated by Smoke Claude
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| issues: read |
There was a problem hiding this comment.
The skip-if-match setting here is a useful guard against duplicate PRs — good practice for mirror workflows that run on a schedule.
| - github | ||
| - "api.github.com" | ||
| - "raw.githubusercontent.com" | ||
| tools: |
There was a problem hiding this comment.
The web-fetch tool listing here is minimal and appropriate — no wildcards, just what's needed for the upstream API calls. Clean permission footprint.
Added in commit 4dfd6ca. The new
The job skips gracefully when |
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/de476d92-cd01-469c-9b3c-8414ef6dfd75 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in commit 5c226a5. Added Also fixed 3 pre-existing TypeScript errors uncovered when
All 148 APM tests pass cleanly. Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
Hey The PR is fully aligned with the contribution guidelines: it's a core-team Copilot-agent PR, it's focused on a single coherent concern, it ships extensive tests, carries a detailed description, and introduces no new dependency-manifest entries. ✅ This PR looks ready for maintainer review.
|
|
@copilot merge main and recompile |
…hub-script-pack-command # Please enter a commit message to explain why this merge is necessary, # especially if it merges an updated upstream into a topic branch. # # Lines starting with '#' will be ignored, and an empty message aborts # the commit. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/2a9921e0-82c7-481d-a818-1ffad473c721 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Done in commits b954f63 (merge) and f89a754 (recompile). Merged Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
…ils in CJS mode Agent-Logs-Url: https://github.com/github/gh-aw/sessions/0c6814ff-57a1-4c17-b499-7ca0c94ae80c Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
The failure was Fixed in commit |
Summary
Implements the APM pack and install commands entirely in JavaScript, removing all dependencies on the Python
apm-clipackage.apm_pack.cjsreplacesmicrosoft/apm-actionfor packing, and the newapm_install.cjsreplacespip install apm-cli+apm installfor package installation. Also adds a daily agentic workflow to keep the JS implementations in sync with the upstreammicrosoft/APMPython source. A new CI parity job validates that the JavaScript implementation produces identical output to the Pythonapm-clireference implementation. A dedicatedmake test-apmtarget provides fast local validation of all APM JavaScript code.What changed
New files
actions/setup/js/apm_install.cjs— JavaScript reimplementation ofapm installusing the GitHub REST API:APM_PACKAGES(JSON array of package slugs) from the environmentGITHUB_APM_PATvia@actions/githubowner/repo[/subpath][#ref]): resolves the ref to a commit SHA, walks the repo tree recursively, filters deployable files under.github/,.claude/,.cursor/,.opencode/, downloads each file toAPM_WORKSPACEapm.yml(workspace metadata) andapm.lock.yaml(resolved dependency manifest) toAPM_WORKSPACEGITHUB_APM_PAT,APM_PACKAGES,APM_WORKSPACEactions/setup/js/run_apm_install.cjs— standalone entry point for CI integration tests and local developmentactions/setup/js/apm_install.test.cjs— 28 vitest tests covering:parsePackageSlug(owner/repo, owner/repo#ref, owner/repo/subpath, owner/repo/subpath#ref)selectDeployableFiles(full package and individual primitive subpath filtering)writeWorkspaceLockfile(YAML output, ISO timestamp quoting, round-trip viaparseAPMLockfile)writeWorkspaceApmYml(workspace metadata)parsePackagesFromEnv(JSON array parsing, error cases)main()integration tests with mocked Octokit (file download, path-traversal rejection, lockfile round-trip)actions/setup/js/apm_pack.cjs— JavaScript implementation ofapm pack --archive, mirroring Python'spacker.py:apm.ymlfor bundle name/versionapm.lock.yamland collectsdeployed_filesfrom all dependenciesAPM_TARGETenv var or auto-detect from workspace folder structure usinglstatSync().isDirectory()for all target folders).github/skills/→.claude/skills/forclaudetarget);CROSS_TARGET_MAPSJSDoc correctly documents src→dst direction..traversaloutput/<name><version>/apm.lock.yamlwithpack:header; all scalar fields serialized throughscalarToYaml()to prevent YAML keyword/number misinterpretation on round-tripis_local) viaLockedDependency.extraso pack is non-destructive.tar.gzarchive viatar -czfand emitsbundle-pathoutput viacore.setOutputAPM_WORKSPACE,APM_BUNDLE_OUTPUT,APM_TARGETactions/setup/js/run_apm_pack.cjs— standalone entry point for CI integration tests and local developmentactions/setup/js/apm_pack.test.cjs— 57 vitest tests.github/workflows/apm-js-mirror-monitor.md— daily Claude agentic workflow that watchesmicrosoft/APMfor upstream changes and keepsapm_pack.cjs/apm_unpack.cjsin syncModified files
pkg/workflow/apm_dependencies.go—GenerateAPMPackStepnow emits a singlegithub-scriptstep (id: apm_pack) that:apm_install.cjs— downloads packages from GitHub using the REST API (no Python, no pip)apm_pack.cjs— creates the.tar.gzbundle and emitsbundle-pathoutputAPM_PACKAGESenv var (single-quoted in the generated YAML to prevent array-literal parsing)safe-output-custom-tokens: truesonpm install @actions/githubruns before thegithub-scriptsteppkg/workflow/compiler_apm_job.go—buildAPMJobnow addsactions/checkout+./actions/setupsteps (withenableCustomTokens=true) before the pack step, ensuring.cjsfiles and@actions/githubare available at runtimeactions/setup/js/apm_unpack.cjs—LockedDependencygains anextra: Record<string, any>field; unknown YAML keys are preserved instead of silently droppedpkg/workflow/apm_dependencies_test.go— updated assertions for the new single-step JS-only structureMakefile— addedmake test-apmtarget that runs the three APM JavaScript test suites (apm_install.test.cjs,apm_pack.test.cjs,apm_unpack.test.cjs) via vitest — 148 tests in ~700msactions/setup/js/tsconfig.json—run_apm_install.cjsandrun_apm_pack.cjsadded to the typecheck exclude list (matching existing exclusion ofrun_apm_unpack.cjs; standalone runners use intentional partial shims).github/workflows/ci.yml— three APM CI jobs:js-apm-integration— pure JS pack+unpack round-trip with a synthetic workspace (no GitHub API)apm-parity-test(new) — installsmicrosoft/apm-sample-packagewith both Pythonapm-cliandapm_install.cjs, packs with both tools, unpacks both bundles withapm_unpack.cjs, and asserts all deployed files are byte-for-byte identical (lockfiles excluded due to expected metadata differences); skips gracefully when no PAT is available.github/workflows/smoke-claude.lock.yml+ golden files — recompiled viamake recompileNo external actions or Python tooling required in generated workflows
Neither
microsoft/apm-actionnor the Pythonapm-clipackage is referenced in any compiled workflow. Package installation and bundle creation are handled entirely by the JavaScript implementation. The new mirror-monitor workflow ensures ongoing parity with future upstream Python changes, and theapm-parity-testCI job provides continuous validation that the JS output matches the Python reference.Local development
Run the full APM test suite with:
make test-apm # runs 148 vitest tests across apm_install, apm_pack, and apm_unpack📍 Connect Copilot coding agent with Jira, Azure Boards or Linear to delegate work to Copilot in one click without leaving your project management tool.
✨ PR Review Safe Output Test - Run 23708939519
✨ PR Review Safe Output Test - Run 23711884225