Skip to content

feat: add pack and restore modes for CI/CD bundle workflows#6

Merged
danielmeppiel merged 6 commits into
mainfrom
feat/pack-restore
Mar 10, 2026
Merged

feat: add pack and restore modes for CI/CD bundle workflows#6
danielmeppiel merged 6 commits into
mainfrom
feat/pack-restore

Conversation

@danielmeppiel

Copy link
Copy Markdown
Collaborator

Summary

Adds two new operational modes to the action — pack and restore — enabling CI/CD workflows that build APM bundles in one job and deploy them in another, without reinstalling APM.

New Inputs

Input Type Description
pack boolean Run apm pack after install/compile steps
bundle string (glob) Path/glob to a pre-built bundle for restore mode
target string Target platform for apm pack --target
archive boolean Create .tar.gz archive (default: true)

New Output

Output Description
bundle-path Absolute path to the generated bundle (for actions/upload-artifact)

How It Works

Pack Mode (pack: 'true')

Runs after existing install/compile steps. Executes apm pack, locates the output bundle, and sets bundle-path output for downstream artifact upload.

Restore Mode (bundle: '*.tar.gz')

Skips APM installation entirely. Resolves the glob to exactly one file, then:

  1. Tries apm unpack (verified extraction)
  2. Falls back to tar xzf (unverified, for environments without APM)

Cross-Job Workflow

jobs:
  build:
    steps:
      - uses: microsoft/apm-action@v1
        id: apm
        with:
          pack: 'true'
          target: vscode
      - uses: actions/upload-artifact@v4
        with:
          name: apm-bundle
          path: ${{ steps.apm.outputs.bundle-path }}

  deploy:
    needs: build
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: apm-bundle
      - uses: microsoft/apm-action@v1
        with:
          bundle: '*.tar.gz'

Implementation

  • src/bundler.ts — New module with resolveLocalBundle(), extractBundle(), runPackStep() + helpers
  • src/runner.ts — Updated flow: restore mode early-exit → existing install → optional pack step
  • Path traversal protection on bundle resolution
  • Mutual exclusivity check: pack and bundle cannot be used together

Testing

  • 12 unit tests in src/__tests__/bundler.test.ts covering all code paths
  • 2 CI integration test jobs: test-pack and test-restore-artifact
  • All existing tests unaffected (fully backwards compatible)

Verification

  • ✅ TypeScript typecheck
  • ✅ ESLint
  • ✅ Jest (12/12 tests pass)
  • ✅ ncc build (dist/index.js)

Notes

  • Downgraded @actions/glob from 0.6.1 → 0.5.0 (v0.6.x is ESM-only, incompatible with Jest/ts-jest CJS environment)
  • No breaking changes — all new inputs are optional with sensible defaults

- Add 'pack' input to run 'apm pack' after install/compile steps
- Add 'bundle' input for restore mode (extract pre-built bundle, skip APM install)
- Add 'target' and 'archive' inputs to control pack behavior
- Add 'bundle-path' output for artifact upload integration
- New src/bundler.ts module with resolveLocalBundle, extractBundle, runPackStep
- Restore mode: tries 'apm unpack' first, falls back to 'tar xzf'
- Path traversal protection on bundle resolution
- Mutual exclusivity check: pack and bundle cannot be used together
- 12 unit tests covering all new code paths
- CI integration tests: test-pack and test-restore-artifact jobs
- Downgrade @actions/glob to 0.5.0 (CJS compat with Jest/ts-jest)
- Updated README with pack mode, restore mode, cross-job examples

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds “pack” and “restore” operational modes to this GitHub Action to enable cross-job APM bundle workflows (produce a bundle in one job, restore primitives in another) while keeping existing install/compile/script behavior intact.

Changes:

  • Introduces new bundling utilities to resolve bundle globs, extract bundles, and run apm pack.
  • Updates the runner flow to early-exit in restore mode and to optionally run pack mode after installation.
  • Adds docs, CI integration jobs, and dependency updates to support glob-based bundle resolution.

Reviewed changes

Copilot reviewed 7 out of 12 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/runner.ts Adds mode detection (pack vs bundle), restore early-exit path, and optional pack step/output.
src/bundler.ts New module implementing bundle resolution (glob), extraction (apm unpack or tar), and pack output discovery.
src/tests/bundler.test.ts Unit tests covering bundle resolving, extraction paths, and pack invocation behavior.
package.json Adds @actions/glob dependency for bundle path globbing.
package-lock.json Locks @actions/glob and transitive deps for reproducible installs.
action.yml Declares new inputs (pack, bundle, target, archive) and new output (bundle-path).
README.md Documents pack/restore usage patterns and cross-job artifact workflow.
.github/workflows/ci.yml Adds integration jobs for pack and restore workflows; updates release job dependencies.
dist/index.js Updates bundled action artifact to include new bundler logic and glob dependency.
dist/bundler.d.ts Generated type declarations for the new bundler module.
dist/runner.d.ts Updates runner doc comments in generated types.
dist/licenses.txt Adds license texts for newly bundled dependencies.
Comments suppressed due to low confidence (1)

src/bundler.ts:82

  • The tar fallback extracts an untrusted archive directly into the workspace. Even with -C and --strip-components, tar archives can contain absolute paths or .. segments that write outside outputDir. Before extracting, list entries (e.g. via tar -tzf) and validate there are no absolute paths / .. / drive prefixes, or extract into a temp dir and only move the expected .github/ + .claude/ directories into place.
  // Fallback: tar extraction
  core.info('APM not available — extracting with tar (no verification)...');
  const rc = await exec.exec('tar', ['xzf', resolvedBundle, '-C', resolvedOutput, '--strip-components=1'], {
    ignoreReturnCode: true,
  });
  if (rc !== 0) {
    throw new Error(`tar extraction failed with exit code ${rc}`);
  }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/bundler.ts Outdated
Comment thread src/bundler.ts Outdated
Comment thread README.md
- Use path.relative() instead of startsWith() for workspace containment
  (prevents prefix bypass like /workspace vs /workspace-evil)
- Error when multiple .tar.gz archives found in build directory
- Error when multiple bundle directories found in build directory
- Add 2 new tests for ambiguous bundle detection (14 total)
- @actions/core 1.11.1 → 2.0.3
- @actions/exec 1.1.1 → 2.0.0
- @actions/io 1.1.3 → 2.0.0

Note: v3.0.0 of these packages are ESM-only, incompatible with
Jest/ts-jest CJS environment and ncc bundling.
- Upgrade @actions/core, exec, io to 3.0.0 and glob to 0.6.1 (ESM)
- Add "type": "module" to package.json
- Set module/moduleResolution to nodenext in tsconfig.json
- Add .js extensions to relative imports (main.ts, runner.ts)
- Rewrite tests with jest.unstable_mockModule and @jest/globals
- Configure jest for ESM (useESM, extensionsToTreatAsEsm)
- Remove @types/jest in favor of @jest/globals
- ncc produces ESM bundle (dist/package.json has type:module)
- Eliminates transitive undici vulnerability (0 vulnerabilities)
- Keeps using: node20 in action.yml for backward compatibility
Add top-level 'permissions: contents: read' to satisfy CodeQL alerts #8, #9, #10.
Only contents:read is needed (for actions/checkout).
- action.yml: using node24 (was node20)
- ci.yml: node-version 24, concurrency groups (cancel-in-progress on PRs),
  timeout-minutes on all jobs (10 build, 5 integration/release)
- package.json: @types/node ^24.0.0
- Rebuilt dist/ with Node 24 to match CI
@danielmeppiel danielmeppiel merged commit 218f922 into main Mar 10, 2026
22 of 25 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.

2 participants