Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 81 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,86 @@ jobs:
fi
echo "✅ Primitives deployed from manifest"

# Integration: update mode -- refresh an existing install via
# `apm update --yes` (the verb swap from issue #46), then prove the
# mutex guard rejects update combined with a no-install mode.
test-update:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v6

- name: Create test apm.yml
run: |
cat > apm.yml << 'EOF'
name: ci-update-test
version: 1.0.0
target: copilot
dependencies:
apm:
- microsoft/apm-sample-package
EOF

- name: Seed an initial install (default verb)
uses: ./

- name: Verify install seeded a resolved lockfile
run: |
if [ ! -f apm.lock.yaml ]; then
echo "::error::install did not create apm.lock.yaml"
exit 1
fi
if ! grep -q 'resolved_commit:' apm.lock.yaml; then
echo "::error::lockfile has no resolved_commit after install"
exit 1
fi
echo "Install seeded apm.lock.yaml with a resolved commit"

- name: Run action in update mode
uses: ./
with:
update: 'true'

- name: Verify update refreshed the project end-to-end
run: |
# The refresh path from issue #46: 'apm update --yes' must keep a
# valid, resolved lockfile and leave the deployed primitives in
# place. A failure here means the verb swap did not run the real
# update flow successfully against the live apm CLI.
if [ ! -f apm.lock.yaml ]; then
echo "::error::update removed apm.lock.yaml"
exit 1
fi
if ! grep -q 'resolved_commit:' apm.lock.yaml; then
echo "::error::lockfile lost resolved_commit after update"
exit 1
fi
if ! find .agents/skills .github -type f \
\( -name 'SKILL.md' -o -name '*.instructions.md' \
-o -name '*.agent.md' -o -name '*.prompt.md' \) 2>/dev/null \
| grep -q .; then
echo "::error::no primitives deployed after update"
exit 1
fi
echo "update mode refreshed lockfile and primitives successfully"

- name: Attempt update + setup-only -- expect failure
id: update-mutex
continue-on-error: true
uses: ./
with:
update: 'true'
setup-only: 'true'

- name: Assert update mutex rejected the conflicting combo
run: |
if [ "${{ steps.update-mutex.outcome }}" != "failure" ]; then
echo "::error::update + setup-only should be rejected, got ${{ steps.update-mutex.outcome }}"
exit 1
fi
echo "update + setup-only correctly rejected by mutex guard"

# Integration: isolated mode (inline deps only, clean .github/)
test-isolated:
needs: build
Expand Down Expand Up @@ -532,7 +612,7 @@ jobs:

# Release: update major version tag (v1 → v1.x.x)
release:
needs: [build, test-manifest, test-isolated, test-compile, test-pack, test-restore-artifact, test-restore-clean-workspace]
needs: [build, test-manifest, test-update, test-isolated, test-compile, test-pack, test-restore-artifact, test-restore-clean-workspace]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 5
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ The floating `v1` tag tracks the latest `1.x` release. Consumers pinning

## [Unreleased]

### Added

- **`update` input to refresh branch/tag dependency refs** ([microsoft/apm-action#46]). When `update: 'true'`, the action runs `apm update --yes` in place of `apm install`: it re-resolves every branch/tag dependency to its latest matching commit, rewrites `apm.lock.yaml`, and then proceeds with the normal post-install steps (`audit-report`, `compile`, `script`, `pack`). The default (`false`) keeps the reproducible install-from-lockfile behaviour. Closes the gap where branch-tracked deps (e.g. `github/awesome-copilot#main`) stayed frozen at the lockfile commit with no first-class way to refresh them — the previous workaround was a hand-rolled `script: "apm update --yes"`. Mutually exclusive with `isolated`, `setup-only`, `bundle`, `bundles-file`, and `mode`.

## [1.9.1] - 2026-05-19

### Fixed
Expand Down Expand Up @@ -88,6 +92,7 @@ APM v0.12.3 made harness detection strict (no more silent default-to-copilot). E
[#33]: https://github.com/microsoft/apm-action/pull/33
[#34]: https://github.com/microsoft/apm-action/pull/34

[microsoft/apm-action#46]: https://github.com/microsoft/apm-action/issues/46
[microsoft/apm#1348]: https://github.com/microsoft/apm/issues/1348

## [1.6.0] - 2026-05-02
Expand Down
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,40 @@ Just install the APM CLI and put it on `PATH`, like `actions/setup-node`. Run an

`setup-only: true` is mutually exclusive with `pack`, `bundle`, and `bundles-file`. The action will not read `apm.yml`, run `apm install`, or deploy primitives. Sets the `apm-version` and `apm-path` outputs so downstream steps can branch on the resolved CLI.

### Update mode (refresh branch/tag refs)

By default the action runs `apm install`, which honours `apm.lock.yaml` and pins each dependency to the exact commit recorded in the lockfile — reproducible by design. Dependencies tracked by a branch or tag (e.g. `github/awesome-copilot#main`) therefore stay frozen until the lockfile is refreshed.

Set `update: 'true'` to run `apm update --yes` instead: it re-resolves every ref to its latest matching commit, rewrites `apm.lock.yaml`, and deploys the refreshed assets. Pair it with a PR-opening step to land the changes:

```yaml
name: Refresh AI agent assets
on:
schedule:
- cron: '0 6 * * 1' # Monday 06:00 UTC
workflow_dispatch:

jobs:
apm-update:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: microsoft/apm-action@v1
with:
update: 'true'
audit-report: 'true'
- name: Open PR if changed
uses: peter-evans/create-pull-request@v6
with:
title: 'chore: update AI agent assets'
branch: apm/auto-update
```

`update: true` composes with `audit-report`, `compile`, `script`, `dependencies`, and `pack` (which all run after the refresh). It requires an `apm.yml` in the working directory to refresh and fails fast if none is present (the `dependencies` input is an additive inline install, not a manifest refresh). It is mutually exclusive with `isolated`, `setup-only`, `bundle`, `bundles-file`, and `mode`, since those either skip the project install or build from scratch with no lockfile to update.

### Bundle format (`apm` vs `plugin`)

`apm pack` supports two layouts:
Expand Down Expand Up @@ -315,6 +349,7 @@ For multi-org or multi-platform scenarios, use the `env:` block for full control
| `dependencies` | No | | YAML array of extra dependencies to install (additive to apm.yml) |
| `isolated` | No | `false` | Ignore apm.yml and clear pre-existing primitive dirs — install only inline dependencies |
| `compile` | No | `false` | Run `apm compile` after install to generate AGENTS.md |
| `update` | No | `false` | Run `apm update --yes` instead of `apm install`: re-resolve branch/tag refs to their latest matching commit and rewrite `apm.lock.yaml`, then run the normal post-install steps. Use in a scheduled/dispatch job that opens a PR with the refreshed assets. Mutually exclusive with `isolated`, `setup-only`, `bundle`, `bundles-file`, and `mode`. |
| `pack` | No | `false` | Pack a bundle after install (produces `.tar.gz` by default) |
| `bundle-format` | No | `apm` | Bundle layout when `pack: true`. `apm` produces an APM bundle (with `apm.lock.yaml` and a `.github/` tree, restorable by this action). `plugin` produces a Claude Code plugin bundle (with `plugin.json` at the root, intended for marketplace consumption). |
| `setup-only` | No | `false` | Install the APM CLI and exit. No `apm.yml` is read, no `apm install` runs, no primitives are deployed. Mutually exclusive with `pack`, `bundle`, and `bundles-file`. |
Expand Down
16 changes: 16 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ inputs:
description: 'Run apm compile after install to generate AGENTS.md'
required: false
default: 'false'
update:
description: |
Re-resolve dependencies instead of installing from the lockfile.
When 'true', the action runs `apm update --yes` in place of
`apm install`: it refreshes branch/tag refs to their latest matching
commit and rewrites apm.lock.yaml, then proceeds with the normal
post-install steps (audit-report, compile, script, pack). Use this in
a scheduled or workflow_dispatch job that opens a PR with the updated
assets. Requires an apm.yml in the working directory to refresh; the
action fails fast if none is present (the dependencies input is an
additive inline install, not a manifest refresh). Defaults to 'false'
(install pinned versions from the lockfile, the reproducible-by-default
behaviour). Mutually exclusive with isolated, setup-only, bundle,
bundles-file, and mode.
required: false
default: 'false'
pack:
description: 'Pack a bundle after install. Format is controlled by `bundle-format` (default: apm). Archived output is .zip by default (apm 0.20+); older CLIs emit .tar.gz. Both are accepted on restore.'
required: false
Expand Down
49 changes: 47 additions & 2 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42520,6 +42520,7 @@ function parseMarketplacePath(raw) {
* With `dependencies` input: parses YAML array, installs each as extra deps (additive to apm.yml).
* With `isolated: true`: clears existing primitives, ignores apm.yml, installs only inline deps.
* With `compile: true`: runs apm compile after install to generate AGENTS.md.
* With `update: true`: runs `apm update --yes` instead of `apm install`, re-resolving refs and rewriting the lockfile.
* With `script` input: runs an apm script after install.
* With `pack: true`: runs apm pack after install to produce a bundle.
* With `bundle` input: restores from a bundle (no APM install needed).
Expand All @@ -42533,6 +42534,13 @@ async function run() {
const bundlesFileInput = lib_core/* getInput */.V4('bundles-file').trim();
const packInput = lib_core/* getInput */.V4('pack') === 'true';
const isolated = lib_core/* getInput */.V4('isolated') === 'true';
// `update: true` swaps the project-install verb from `apm install` to
// `apm update --yes`, re-resolving branch/tag refs and rewriting the
// lockfile (microsoft/apm-action#46). It only makes sense in the
// default project-install flow, so it is rejected in the no-install
// modes (setup-only / bundle / bundles-file / mode) and in isolated
// mode (a from-scratch install with no pre-existing lockfile to update).
const update = lib_core/* getInput */.V4('update') === 'true';
// Default `packages` output to '[]' so downstream `fromJSON()` steps
// can parse it unconditionally regardless of mode. mode: release
// overwrites this with the actual JSON array of packed artifacts.
Expand All @@ -42556,6 +42564,8 @@ async function run() {
modeConflicts.push('bundles-file');
if (lib_core/* getInput */.V4('setup-only') === 'true')
modeConflicts.push('setup-only');
if (update)
modeConflicts.push('update');
if (modeConflicts.length > 0) {
throw new Error(`mode='${modeInput}' is mutually exclusive with: ${modeConflicts.join(', ')}. `
+ `mode runs a fixed pipeline; remove the conflicting flag(s) or unset mode.`);
Expand Down Expand Up @@ -42704,6 +42714,8 @@ async function run() {
conflicts.push('include-prerelease');
if (lib_core/* getInput */.V4('mode').trim())
conflicts.push('mode');
if (update)
conflicts.push('update');
if (conflicts.length > 0) {
throw new Error(`'setup-only' is mutually exclusive with: ${conflicts.join(', ')}. `
+ `setup-only installs the APM CLI onto PATH and exits; remove the `
Expand Down Expand Up @@ -42735,6 +42747,23 @@ async function run() {
throw new Error(`inputs 'pack', 'bundle', and 'bundles-file' are mutually exclusive `
+ `(got: ${modeFlags.join(', ')}). Pick exactly one mode per step.`);
}
// `update` mutex: it swaps the project-install verb and is meaningless
// in the no-install restore modes and in from-scratch isolated installs.
// Fail fast with a clear message rather than silently ignoring the flag.
if (update) {
const updateConflicts = [];
if (isolated)
updateConflicts.push('isolated');
if (bundleInput)
updateConflicts.push('bundle');
if (bundlesFileInput)
updateConflicts.push('bundles-file');
if (updateConflicts.length > 0) {
throw new Error(`'update' is mutually exclusive with: ${updateConflicts.join(', ')}. `
+ `update re-resolves dependencies in the project-install flow; `
+ `remove the conflicting input(s) or set update: false.`);
}
}
// Reject pack pass-through inputs outside pack mode early, so they
// are not silently ignored in bundle / bundles-file restore paths or
// in the default install flow. Matches the setup-only conflict shape.
Expand Down Expand Up @@ -42893,10 +42922,26 @@ async function run() {
await runApm(['install'], resolvedDir);
}
else {
// Default: install from apm.yml (if present), then add inline deps
// Default: install (or update) from apm.yml (if present), then add
// inline deps. `update: true` swaps `apm install` for `apm update
// --yes`, re-resolving branch/tag refs and rewriting the lockfile
// non-interactively (microsoft/apm-action#46).
const apmYmlPath = external_path_.join(resolvedDir, 'apm.yml');
// `update` is a project-manifest refresh: it re-resolves the refs
// declared in apm.yml and rewrites apm.lock.yaml. With no apm.yml
// there is nothing to refresh, so fail fast rather than silently
// ignoring the flag -- the `dependencies` input is an additive
// inline install, not a manifest refresh.
if (update && !external_fs_.existsSync(apmYmlPath)) {
throw new Error(`'update: true' requires an apm.yml in the working directory to `
+ `refresh, but none was found at ${apmYmlPath}. 'apm update' `
+ `re-resolves apm.yml refs and rewrites apm.lock.yaml; the `
+ `'dependencies' input is an additive inline install, not a `
+ `manifest refresh. Commit an apm.yml, or remove update: true.`);
}
const projectVerb = update ? ['update', '--yes'] : ['install'];
if (external_fs_.existsSync(apmYmlPath) || !depsInput) {
await runApm(['install'], resolvedDir);
await runApm(projectVerb, resolvedDir);
}
// Install extra inline deps additively
if (depsInput) {
Expand Down
1 change: 1 addition & 0 deletions dist/runner.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* With `dependencies` input: parses YAML array, installs each as extra deps (additive to apm.yml).
* With `isolated: true`: clears existing primitives, ignores apm.yml, installs only inline deps.
* With `compile: true`: runs apm compile after install to generate AGENTS.md.
* With `update: true`: runs `apm update --yes` instead of `apm install`, re-resolving refs and rewriting the lockfile.
* With `script` input: runs an apm script after install.
* With `pack: true`: runs apm pack after install to produce a bundle.
* With `bundle` input: restores from a bundle (no APM install needed).
Expand Down
Loading
Loading