From d22fd5779fe1d15151d0e2483ceb4b7e0d548884 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 14:52:00 +0000
Subject: [PATCH 1/4] feat: move actions-lock.json to
.github/workflows/aw-lock.yml with new YAML format
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Change CacheFileName to aw-lock.yml in .github/workflows/ (was actions-lock.json in .github/aw/)
- Add LegacyCacheFileName constant for backward-compat loading
- Extend format: version, actions (was entries), containers sections
- Add ContainerPinEntry struct for future container pin support
- Keep backward compat: Load() falls back to old .github/aw/actions-lock.json
- Add MarkDirty() method to force save after legacy load
- Add codemod_actions_lock_migration.go: MigrateActionsLockFile() for fix --write
- Wire migration into fix_command.go and fix_codemods.go registry
- Update update_actions.go, update_command.go, upgrade_command.go references
- Update Makefile sync-action-pins to use Go script that converts YAML→JSON
- Add scripts/sync-action-pins/main.go for the conversion
- Migrate .github/aw/actions-lock.json → .github/workflows/aw-lock.yml
- Migrate pkg/workflow/.github/aw/actions-lock.json → pkg/workflow/.github/workflows/aw-lock.yml
- Update tests: action_cache_test.go, update_command_test.go, compile_integration_test.go
- Update docs and scratchpad references
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/14c1b3fc-d16d-42e6-9307-d403b1bf6880
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/aw/actions-lock.json | 187 --------------
.github/workflows/aw-lock.yml | 148 ++++++++++++
Makefile | 14 +-
.../docs/introduction/architecture.mdx | 2 +-
.../docs/reference/compilation-process.md | 12 +-
docs/src/content/docs/reference/faq.md | 8 +-
docs/src/content/docs/reference/releases.md | 6 +-
docs/src/content/docs/setup/cli.md | 2 +-
pkg/cli/codemod_actions_lock_migration.go | 86 +++++++
pkg/cli/compile_integration_test.go | 23 +-
pkg/cli/fix_codemods.go | 1 +
pkg/cli/fix_command.go | 14 +-
pkg/cli/update_actions.go | 24 +-
pkg/cli/update_command.go | 6 +-
pkg/cli/update_command_test.go | 14 +-
pkg/cli/upgrade_command.go | 2 +-
pkg/workflow/.github/aw/actions-lock.json | 34 ---
pkg/workflow/.github/workflows/aw-lock.yml | 27 +++
pkg/workflow/action_cache.go | 228 ++++++++++++++----
pkg/workflow/action_cache_test.go | 26 +-
pkg/workflow/action_sha_validation_test.go | 2 +-
pkg/workflow/compiler_types.go | 2 +-
pkg/workflow/data/action_pins.json | 3 +-
pkg/workflow/safe_outputs_actions.go | 4 +-
scratchpad/debugging-action-pinning.md | 36 +--
scratchpad/layout.md | 4 +-
scripts/sync-action-pins/main.go | 61 +++++
27 files changed, 604 insertions(+), 372 deletions(-)
delete mode 100644 .github/aw/actions-lock.json
create mode 100644 .github/workflows/aw-lock.yml
create mode 100644 pkg/cli/codemod_actions_lock_migration.go
delete mode 100644 pkg/workflow/.github/aw/actions-lock.json
create mode 100644 pkg/workflow/.github/workflows/aw-lock.yml
create mode 100644 scripts/sync-action-pins/main.go
diff --git a/.github/aw/actions-lock.json b/.github/aw/actions-lock.json
deleted file mode 100644
index 4432457f3b3..00000000000
--- a/.github/aw/actions-lock.json
+++ /dev/null
@@ -1,187 +0,0 @@
-{
- "entries": {
- "actions-ecosystem/action-add-labels@v1.1.3": {
- "repo": "actions-ecosystem/action-add-labels",
- "version": "v1.1.3",
- "sha": "c96b68fec76a0987cd93957189e9abd0b9a72ff1",
- "inputs": {
- "github_token": {
- "description": "A GitHub token.",
- "default": "${{ github.token }}"
- },
- "labels": {
- "description": "The labels' name to be added. Must be separated with line breaks if there're multiple labels.",
- "required": true
- },
- "number": {
- "description": "The number of the issue or pull request."
- },
- "repo": {
- "description": "The owner and repository name. e.g.) Codertocat/Hello-World",
- "default": "${{ github.repository }}"
- }
- },
- "action_description": "Add labels to an issue or a pull request."
- },
- "actions/ai-inference@v2.0.8": {
- "repo": "actions/ai-inference",
- "version": "v2.0.8",
- "sha": "af6ad2c4ac4edf01884054fc3a6caa6d2567c13a"
- },
- "actions/attest-build-provenance@v4.1.0": {
- "repo": "actions/attest-build-provenance",
- "version": "v4.1.0",
- "sha": "a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32"
- },
- "actions/cache/restore@v5.0.4": {
- "repo": "actions/cache/restore",
- "version": "v5.0.4",
- "sha": "668228422ae6a00e4ad889ee87cd7109ec5666a7"
- },
- "actions/cache/save@v5.0.4": {
- "repo": "actions/cache/save",
- "version": "v5.0.4",
- "sha": "668228422ae6a00e4ad889ee87cd7109ec5666a7"
- },
- "actions/cache@v5.0.4": {
- "repo": "actions/cache",
- "version": "v5.0.4",
- "sha": "668228422ae6a00e4ad889ee87cd7109ec5666a7"
- },
- "actions/checkout@v6.0.2": {
- "repo": "actions/checkout",
- "version": "v6.0.2",
- "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd"
- },
- "actions/create-github-app-token@v3": {
- "repo": "actions/create-github-app-token",
- "version": "v3",
- "sha": "f8d387b68d61c58ab83c6c016672934102569859"
- },
- "actions/download-artifact@v8.0.1": {
- "repo": "actions/download-artifact",
- "version": "v8.0.1",
- "sha": "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c"
- },
- "actions/github-script@v8": {
- "repo": "actions/github-script",
- "version": "v8",
- "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd"
- },
- "actions/setup-dotnet@v5.2.0": {
- "repo": "actions/setup-dotnet",
- "version": "v5.2.0",
- "sha": "c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7"
- },
- "actions/setup-go@v6.4.0": {
- "repo": "actions/setup-go",
- "version": "v6.4.0",
- "sha": "4a3601121dd01d1626a1e23e37211e3254c1c06c"
- },
- "actions/setup-java@v5.2.0": {
- "repo": "actions/setup-java",
- "version": "v5.2.0",
- "sha": "be666c2fcd27ec809703dec50e508c2fdc7f6654"
- },
- "actions/setup-node@v6.3.0": {
- "repo": "actions/setup-node",
- "version": "v6.3.0",
- "sha": "53b83947a5a98c8d113130e565377fae1a50d02f"
- },
- "actions/setup-python@v6.2.0": {
- "repo": "actions/setup-python",
- "version": "v6.2.0",
- "sha": "a309ff8b426b58ec0e2a45f0f869d46889d02405"
- },
- "actions/upload-artifact@v7": {
- "repo": "actions/upload-artifact",
- "version": "v7",
- "sha": "bbbca2ddaa5d8feaa63e36b76fdaad77386f024f"
- },
- "anchore/sbom-action@v0.24.0": {
- "repo": "anchore/sbom-action",
- "version": "v0.24.0",
- "sha": "e22c389904149dbc22b58101806040fa8d37a610"
- },
- "astral-sh/setup-uv@v8.0.0": {
- "repo": "astral-sh/setup-uv",
- "version": "v8.0.0",
- "sha": "cec208311dfd045dd5311c1add060b2062131d57"
- },
- "cli/gh-extension-precompile@v2.1.0": {
- "repo": "cli/gh-extension-precompile",
- "version": "v2.1.0",
- "sha": "9e2237c30f869ad3bcaed6a4be2cd43564dd421b"
- },
- "denoland/setup-deno@v2.0.4": {
- "repo": "denoland/setup-deno",
- "version": "v2.0.4",
- "sha": "667a34cdef165d8d2b2e98dde39547c9daac7282"
- },
- "docker/build-push-action@v7": {
- "repo": "docker/build-push-action",
- "version": "v7",
- "sha": "d08e5c354a6adb9ed34480a06d141179aa583294"
- },
- "docker/login-action@v4.1.0": {
- "repo": "docker/login-action",
- "version": "v4.1.0",
- "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121"
- },
- "docker/metadata-action@v6": {
- "repo": "docker/metadata-action",
- "version": "v6",
- "sha": "030e881283bb7a6894de51c315a6bfe6a94e05cf"
- },
- "docker/setup-buildx-action@v4": {
- "repo": "docker/setup-buildx-action",
- "version": "v4",
- "sha": "4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd"
- },
- "erlef/setup-beam@v1.24": {
- "repo": "erlef/setup-beam",
- "version": "v1.24",
- "sha": "3077b49d03fe1e281e5e5bc79084f079e9bf93ca"
- },
- "github/codeql-action/upload-sarif@v4.35.1": {
- "repo": "github/codeql-action/upload-sarif",
- "version": "v4.35.1",
- "sha": "0e9f55954318745b37b7933c693bc093f7336125"
- },
- "github/gh-aw-actions/setup@v0.67.2": {
- "repo": "github/gh-aw-actions/setup",
- "version": "v0.67.2",
- "sha": "03e31e064a68e8d5ad890c92f303cfb5a3536006"
- },
- "github/stale-repos@v9.0.7": {
- "repo": "github/stale-repos",
- "version": "v9.0.7",
- "sha": "25946246f29e8692a397502045e457c4dc96c6e4"
- },
- "haskell-actions/setup@v2.10.3": {
- "repo": "haskell-actions/setup",
- "version": "v2.10.3",
- "sha": "9cd1b7bf3f36d5a3c3b17abc3545bfb5481912ea"
- },
- "microsoft/apm-action@v1.4.1": {
- "repo": "microsoft/apm-action",
- "version": "v1.4.1",
- "sha": "a190b0b1a91031057144dc136acf9757a59c9e4d"
- },
- "oven-sh/setup-bun@v2.2.0": {
- "repo": "oven-sh/setup-bun",
- "version": "v2.2.0",
- "sha": "0c5077e51419868618aeaa5fe8019c62421857d6"
- },
- "ruby/setup-ruby@v1.300.0": {
- "repo": "ruby/setup-ruby",
- "version": "v1.300.0",
- "sha": "e65c17d16e57e481586a6a5a0282698790062f92"
- },
- "super-linter/super-linter@v8.6.0": {
- "repo": "super-linter/super-linter",
- "version": "v8.6.0",
- "sha": "9e863354e3ff62e0727d37183162c4a88873df41"
- }
- }
-}
diff --git a/.github/workflows/aw-lock.yml b/.github/workflows/aw-lock.yml
new file mode 100644
index 00000000000..36c6fdb9668
--- /dev/null
+++ b/.github/workflows/aw-lock.yml
@@ -0,0 +1,148 @@
+version: "1"
+actions:
+ 'actions-ecosystem/action-add-labels@v1.1.3':
+ repo: actions-ecosystem/action-add-labels
+ version: v1.1.3
+ sha: c96b68fec76a0987cd93957189e9abd0b9a72ff1
+ inputs:
+ github_token:
+ description: A GitHub token.
+ default: ${{ github.token }}
+ labels:
+ description: The labels' name to be added. Must be separated with line breaks if there're multiple labels.
+ required: true
+ number:
+ description: The number of the issue or pull request.
+ repo:
+ description: The owner and repository name. e.g.) Codertocat/Hello-World
+ default: ${{ github.repository }}
+ action_description: Add labels to an issue or a pull request.
+ 'actions/ai-inference@v2.0.8':
+ repo: actions/ai-inference
+ version: v2.0.8
+ sha: af6ad2c4ac4edf01884054fc3a6caa6d2567c13a
+ 'actions/attest-build-provenance@v4.1.0':
+ repo: actions/attest-build-provenance
+ version: v4.1.0
+ sha: a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32
+ 'actions/cache/restore@v5.0.4':
+ repo: actions/cache/restore
+ version: v5.0.4
+ sha: 668228422ae6a00e4ad889ee87cd7109ec5666a7
+ 'actions/cache/save@v5.0.4':
+ repo: actions/cache/save
+ version: v5.0.4
+ sha: 668228422ae6a00e4ad889ee87cd7109ec5666a7
+ 'actions/cache@v5.0.4':
+ repo: actions/cache
+ version: v5.0.4
+ sha: 668228422ae6a00e4ad889ee87cd7109ec5666a7
+ 'actions/checkout@v6.0.2':
+ repo: actions/checkout
+ version: v6.0.2
+ sha: de0fac2e4500dabe0009e67214ff5f5447ce83dd
+ 'actions/create-github-app-token@v3':
+ repo: actions/create-github-app-token
+ version: v3
+ sha: f8d387b68d61c58ab83c6c016672934102569859
+ 'actions/download-artifact@v8.0.1':
+ repo: actions/download-artifact
+ version: v8.0.1
+ sha: 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
+ 'actions/github-script@v8':
+ repo: actions/github-script
+ version: v8
+ sha: ed597411d8f924073f98dfc5c65a23a2325f34cd
+ 'actions/setup-dotnet@v5.2.0':
+ repo: actions/setup-dotnet
+ version: v5.2.0
+ sha: c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7
+ 'actions/setup-go@v6.4.0':
+ repo: actions/setup-go
+ version: v6.4.0
+ sha: 4a3601121dd01d1626a1e23e37211e3254c1c06c
+ 'actions/setup-java@v5.2.0':
+ repo: actions/setup-java
+ version: v5.2.0
+ sha: be666c2fcd27ec809703dec50e508c2fdc7f6654
+ 'actions/setup-node@v6.3.0':
+ repo: actions/setup-node
+ version: v6.3.0
+ sha: 53b83947a5a98c8d113130e565377fae1a50d02f
+ 'actions/setup-python@v6.2.0':
+ repo: actions/setup-python
+ version: v6.2.0
+ sha: a309ff8b426b58ec0e2a45f0f869d46889d02405
+ 'actions/upload-artifact@v7':
+ repo: actions/upload-artifact
+ version: v7
+ sha: bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
+ 'anchore/sbom-action@v0.24.0':
+ repo: anchore/sbom-action
+ version: v0.24.0
+ sha: e22c389904149dbc22b58101806040fa8d37a610
+ 'astral-sh/setup-uv@v8.0.0':
+ repo: astral-sh/setup-uv
+ version: v8.0.0
+ sha: cec208311dfd045dd5311c1add060b2062131d57
+ 'cli/gh-extension-precompile@v2.1.0':
+ repo: cli/gh-extension-precompile
+ version: v2.1.0
+ sha: 9e2237c30f869ad3bcaed6a4be2cd43564dd421b
+ 'denoland/setup-deno@v2.0.4':
+ repo: denoland/setup-deno
+ version: v2.0.4
+ sha: 667a34cdef165d8d2b2e98dde39547c9daac7282
+ 'docker/build-push-action@v7':
+ repo: docker/build-push-action
+ version: v7
+ sha: d08e5c354a6adb9ed34480a06d141179aa583294
+ 'docker/login-action@v4.1.0':
+ repo: docker/login-action
+ version: v4.1.0
+ sha: 4907a6ddec9925e35a0a9e82d7399ccc52663121
+ 'docker/metadata-action@v6':
+ repo: docker/metadata-action
+ version: v6
+ sha: 030e881283bb7a6894de51c315a6bfe6a94e05cf
+ 'docker/setup-buildx-action@v4':
+ repo: docker/setup-buildx-action
+ version: v4
+ sha: 4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd
+ 'erlef/setup-beam@v1.24':
+ repo: erlef/setup-beam
+ version: v1.24
+ sha: 3077b49d03fe1e281e5e5bc79084f079e9bf93ca
+ 'github/codeql-action/upload-sarif@v4.35.1':
+ repo: github/codeql-action/upload-sarif
+ version: v4.35.1
+ sha: 0e9f55954318745b37b7933c693bc093f7336125
+ 'github/gh-aw-actions/setup@v0.67.2':
+ repo: github/gh-aw-actions/setup
+ version: v0.67.2
+ sha: 03e31e064a68e8d5ad890c92f303cfb5a3536006
+ 'github/stale-repos@v9.0.7':
+ repo: github/stale-repos
+ version: v9.0.7
+ sha: 25946246f29e8692a397502045e457c4dc96c6e4
+ 'haskell-actions/setup@v2.10.3':
+ repo: haskell-actions/setup
+ version: v2.10.3
+ sha: 9cd1b7bf3f36d5a3c3b17abc3545bfb5481912ea
+ 'microsoft/apm-action@v1.4.1':
+ repo: microsoft/apm-action
+ version: v1.4.1
+ sha: a190b0b1a91031057144dc136acf9757a59c9e4d
+ 'oven-sh/setup-bun@v2.2.0':
+ repo: oven-sh/setup-bun
+ version: v2.2.0
+ sha: 0c5077e51419868618aeaa5fe8019c62421857d6
+ 'ruby/setup-ruby@v1.300.0':
+ repo: ruby/setup-ruby
+ version: v1.300.0
+ sha: e65c17d16e57e481586a6a5a0282698790062f92
+ 'super-linter/super-linter@v8.6.0':
+ repo: super-linter/super-linter
+ version: v8.6.0
+ sha: 9e863354e3ff62e0727d37183162c4a88873df41
+
diff --git a/Makefile b/Makefile
index d9dd91b5a6d..4bf9d6c0ef5 100644
--- a/Makefile
+++ b/Makefile
@@ -665,16 +665,12 @@ clean-docs:
@echo "✓ Documentation artifacts cleaned"
# Sync templates from .github to pkg/cli/templates
-# Sync action pins from .github/aw to pkg/workflow/data
+# Sync action pins from .github/workflows/aw-lock.yml to pkg/workflow/data
.PHONY: sync-action-pins
sync-action-pins:
- @echo "Syncing actions-lock.json from .github/aw to pkg/workflow/data/action_pins.json..."
- @if [ -f .github/aw/actions-lock.json ]; then \
- cp .github/aw/actions-lock.json pkg/workflow/data/action_pins.json; \
- echo "✓ Action pins synced successfully"; \
- else \
- echo "⚠ Warning: .github/aw/actions-lock.json does not exist yet"; \
- fi
+ @echo "Syncing action pins from .github/workflows/aw-lock.yml to pkg/workflow/data/action_pins.json..."
+ @go run ./scripts/sync-action-pins
+ @echo "✓ Action pins synced successfully"
# Sync action scripts
.PHONY: sync-action-scripts
@@ -806,7 +802,7 @@ help:
@echo " actionlint - Validate workflows with actionlint (depends on build)"
@echo " validate-workflows - Validate compiled workflow lock files (depends on build)"
@echo " install - Install binary locally"
- @echo " sync-action-pins - Sync actions-lock.json from .github/aw to pkg/workflow/data (runs automatically during build)"
+ @echo " sync-action-pins - Sync action pins from .github/workflows/aw-lock.yml to pkg/workflow/data (runs automatically during build)"
@echo " sync-action-scripts - Sync install-gh-aw.sh to actions/setup-cli/install.sh (runs automatically during build)"
@echo " update - Update GitHub Actions and workflows, sync action pins, and rebuild binary"
@echo " fix - Apply automatic codemod-style fixes to workflow files (depends on build)"
diff --git a/docs/src/content/docs/introduction/architecture.mdx b/docs/src/content/docs/introduction/architecture.mdx
index 0eb27820eb1..81fc5033bdb 100644
--- a/docs/src/content/docs/introduction/architecture.mdx
+++ b/docs/src/content/docs/introduction/architecture.mdx
@@ -416,7 +416,7 @@ flowchart TB
subgraph Pinning["Action Pinning"]
SHA["SHA Resolution
actions/checkout@sha # v4"]
- CACHE[/"actions-lock.json
(Cached SHAs)"/]
+ CACHE[/"aw-lock.yml
(Cached SHAs)"/]
end
subgraph Scanners["Security Scanners"]
diff --git a/docs/src/content/docs/reference/compilation-process.md b/docs/src/content/docs/reference/compilation-process.md
index 23962e23873..25a6e045d77 100644
--- a/docs/src/content/docs/reference/compilation-process.md
+++ b/docs/src/content/docs/reference/compilation-process.md
@@ -203,15 +203,15 @@ Data flows via GitHub Actions artifacts: agent writes `agent_output.json` → de
## Action Pinning
-All GitHub Actions are pinned to commit SHAs (e.g., `actions/checkout@b4ffde6...11 # v6`) to prevent supply chain attacks. Tags can be moved to malicious commits, but SHA commits are immutable. The resolution order mirrors Phase 4: cache (`.github/aw/actions-lock.json`) → GitHub API → embedded pins.
+All GitHub Actions are pinned to commit SHAs (e.g., `actions/checkout@b4ffde6...11 # v6`) to prevent supply chain attacks. Tags can be moved to malicious commits, but SHA commits are immutable. The resolution order mirrors Phase 4: cache (`.github/workflows/aw-lock.yml`) → GitHub API → embedded pins.
-### The actions-lock.json Cache
+### The aw-lock.yml Cache
-`.github/aw/actions-lock.json` stores resolved `action@version` → SHA mappings so that compilation produces consistent results regardless of the token available. Resolving a version tag to a SHA requires querying the GitHub API, which can fail when the token has limited permissions — notably when compiling via GitHub Copilot Coding Agent (CCA), which uses a restricted token that may not have access to external repositories.
+`.github/workflows/aw-lock.yml` stores resolved `action@version` → SHA mappings so that compilation produces consistent results regardless of the token available. Resolving a version tag to a SHA requires querying the GitHub API, which can fail when the token has limited permissions — notably when compiling via GitHub Copilot Coding Agent (CCA), which uses a restricted token that may not have access to external repositories.
By caching SHA resolutions from a prior compilation (done with a user PAT or a GitHub Actions token with broader scope), subsequent compilations reuse those SHAs without making API calls. Without the cache, compilation is unstable: it succeeds with a permissive token but fails when token access is restricted.
-**Commit `actions-lock.json` to version control.** This ensures all contributors and automated tools, including CCA, use the same immutable pins. Refresh it periodically with `gh aw update-actions`, or delete it and recompile with an appropriate token to force full re-resolution.
+**Commit `aw-lock.yml` to version control.** This ensures all contributors and automated tools, including CCA, use the same immutable pins. Refresh it periodically with `gh aw update-actions`, or delete it and recompile with an appropriate token to force full re-resolution.
## The gh-aw-actions Repository
@@ -316,7 +316,7 @@ Pre-activation runs checks sequentially. Any failure sets `activated=false`, pre
## Performance Optimization
-**Compilation speed**: Simple workflows compile in ~100ms, complex workflows with imports in ~500ms, and workflows with dynamic action resolution in ~2s. Optimize by using action cache (`.github/aw/actions-lock.json`), minimizing import depth, and pre-compiling shared workflows.
+**Compilation speed**: Simple workflows compile in ~100ms, complex workflows with imports in ~500ms, and workflows with dynamic action resolution in ~2s. Optimize by using action cache (`.github/workflows/aw-lock.yml`), minimizing import depth, and pre-compiling shared workflows.
**Runtime performance**: Safe output jobs without dependencies run in parallel. Enable `cache:` for dependencies, use `cache-memory:` for persistent agent memory, and cache action resolutions for faster compilation.
@@ -332,7 +332,7 @@ Pre-activation runs checks sequentially. Any failure sets `activated=false`, pre
**Security**: Always use action pinning (never floating tags), enable threat detection (`safe-outputs.threat-detection:`), limit tool access with `allowed:`, review generated `.lock.yml` files, and run security scanners (`--actionlint --zizmor --poutine`).
-**Maintainability**: Use imports for shared configuration, document complex workflows with `description:`, compile frequently during development, version control lock files and action pins (`.github/aw/actions-lock.json`).
+**Maintainability**: Use imports for shared configuration, document complex workflows with `description:`, compile frequently during development, version control lock files and action pins (`.github/workflows/aw-lock.yml`).
**Performance**: Enable caching (`cache:` and `cache-memory:`), minimize imports to essentials, optimize tool configurations with restricted `allowed:` lists, use safe-jobs for custom logic.
diff --git a/docs/src/content/docs/reference/faq.md b/docs/src/content/docs/reference/faq.md
index 9cd953ad3e1..67963f13767 100644
--- a/docs/src/content/docs/reference/faq.md
+++ b/docs/src/content/docs/reference/faq.md
@@ -270,13 +270,13 @@ Both files should be committed to version control:
- **`.md` file**: Your source - edit the prompt body freely; changes take effect at the next run without recompiling
- **`.lock.yml` file**: The compiled workflow GitHub Actions actually runs; must be regenerated after any frontmatter changes (permissions, tools, triggers)
-### What is the actions-lock.json file?
+### What is the aw-lock.yml file?
-The `.github/aw/actions-lock.json` file is a cache of resolved `action@version` → ref mappings. During compilation, the compiler **tries** to pin each action reference to an immutable commit SHA for security. Resolving a version tag to a SHA requires querying the GitHub API (scanning releases), which can fail when the available token has limited permissions — for example, when compiling via GitHub Copilot Coding Agent (CCA) where the token may not have access to external repositories. In those cases, the compiler may fall back to leaving a stable version tag ref (such as `@v0`) instead of a SHA.
+The `.github/workflows/aw-lock.yml` file is a cache of resolved `action@version` → ref mappings. During compilation, the compiler **tries** to pin each action reference to an immutable commit SHA for security. Resolving a version tag to a SHA requires querying the GitHub API (scanning releases), which can fail when the available token has limited permissions — for example, when compiling via GitHub Copilot Coding Agent (CCA) where the token may not have access to external repositories. In those cases, the compiler may fall back to leaving a stable version tag ref (such as `@v0`) instead of a SHA.
-The cache avoids this problem: if a ref (typically a SHA) was previously resolved (using a user PAT or a GitHub Actions token with broader access), the result is stored in `actions-lock.json` and reused on subsequent compilations, regardless of the current token's capabilities. Without this cache, compilation is unstable — it succeeds with a permissive token but fails when token access is restricted.
+The cache avoids this problem: if a ref (typically a SHA) was previously resolved (using a user PAT or a GitHub Actions token with broader access), the result is stored in `aw-lock.yml` and reused on subsequent compilations, regardless of the current token's capabilities. Without this cache, compilation is unstable — it succeeds with a permissive token but fails when token access is restricted.
-Commit `actions-lock.json` to version control so that all contributors and automated tools (including CCA) use consistent action refs (SHAs or version tags) without needing to re-resolve them. Refresh the cache periodically with `gh aw update-actions`, or delete it and recompile to force a full re-resolution when you have an appropriate token. See [Action Pinning](/gh-aw/reference/compilation-process/#action-pinning) for details.
+Commit `aw-lock.yml` to version control so that all contributors and automated tools (including CCA) use consistent action refs (SHAs or version tags) without needing to re-resolve them. Refresh the cache periodically with `gh aw update-actions`, or delete it and recompile to force a full re-resolution when you have an appropriate token. See [Action Pinning](/gh-aw/reference/compilation-process/#action-pinning) for details.
### What is `github/gh-aw-actions`?
diff --git a/docs/src/content/docs/reference/releases.md b/docs/src/content/docs/reference/releases.md
index d56ecfc5158..0f66ffbb78d 100644
--- a/docs/src/content/docs/reference/releases.md
+++ b/docs/src/content/docs/reference/releases.md
@@ -81,12 +81,12 @@ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
SHA pins are immutable — unlike tags, they cannot be silently redirected to a different commit. This protects workflows from supply-chain attacks.
-The resolved SHA mappings are cached in `.github/aw/actions-lock.json`. Commit this file to version control so that all contributors and automated tools (including GitHub Copilot Coding Agent) produce identical lock files without needing broad API access.
+The resolved SHA mappings are cached in `.github/workflows/aw-lock.yml`. Commit this file to version control so that all contributors and automated tools (including GitHub Copilot Coding Agent) produce identical lock files without needing broad API access.
To refresh action pins:
```bash
-gh aw update-actions # Update actions-lock.json to latest SHAs
+gh aw update-actions # Update aw-lock.yml to latest SHAs
gh aw compile # Recompile workflows using the refreshed pins
```
@@ -104,7 +104,7 @@ These two commands address different concerns:
1. Self-updates the `gh aw` extension to the latest version
2. Regenerates the dispatcher agent file (like `gh aw init`)
3. Applies codemods to fix deprecated syntax across all workflow markdown files
-4. Updates GitHub Actions versions in `actions-lock.json`
+4. Updates GitHub Actions versions in `aw-lock.yml`
5. Recompiles all workflows to produce fresh `.lock.yml` files
Run `upgrade` after installing a new version of `gh aw`, or periodically to keep your repository current.
diff --git a/docs/src/content/docs/setup/cli.md b/docs/src/content/docs/setup/cli.md
index cc8e01fe5b6..8d89fe0becf 100644
--- a/docs/src/content/docs/setup/cli.md
+++ b/docs/src/content/docs/setup/cli.md
@@ -534,7 +534,7 @@ gh aw remove my-workflow --keep-orphans # Remove but keep orphaned include file
Update workflows based on `source` field (`owner/repo/path@ref`). By default, performs a 3-way merge to preserve local changes; use `--no-merge` to override with upstream. Semantic versions update within same major version.
-By default, `update` also force-updates all GitHub Actions referenced in your workflows (both in `actions-lock.json` and workflow files) to their latest major version. Use `--disable-release-bump` to restrict force-updates to core `actions/*` actions only.
+By default, `update` also force-updates all GitHub Actions referenced in your workflows (both in `aw-lock.yml` and workflow files) to their latest major version. Use `--disable-release-bump` to restrict force-updates to core `actions/*` actions only.
If no workflows in the repository contain a `source` field, the command exits gracefully with an informational message rather than an error. This is expected behavior for repositories that have not yet added updatable workflows.
diff --git a/pkg/cli/codemod_actions_lock_migration.go b/pkg/cli/codemod_actions_lock_migration.go
new file mode 100644
index 00000000000..679c6c62beb
--- /dev/null
+++ b/pkg/cli/codemod_actions_lock_migration.go
@@ -0,0 +1,86 @@
+package cli
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/github/gh-aw/pkg/console"
+ "github.com/github/gh-aw/pkg/workflow"
+)
+
+// getActionsLockMigrationCodemod returns a file-level codemod that migrates the
+// old .github/aw/actions-lock.json to the new .github/workflows/aw-lock.yml format.
+// The Apply function is a no-op (it doesn't modify workflow files); the actual
+// migration is performed by MigrateActionsLockFile which is called from fix_command.go.
+func getActionsLockMigrationCodemod() Codemod {
+ return Codemod{
+ ID: "migrate-actions-lock-file",
+ Name: "Migrate actions-lock.json to aw-lock.yml",
+ Description: "Moves .github/aw/actions-lock.json to .github/workflows/aw-lock.yml with the new YAML format",
+ IntroducedIn: "0.71.0",
+ Apply: func(content string, frontmatter map[string]any) (string, bool, error) {
+ // This codemod is handled by MigrateActionsLockFile (called from fix_command.go).
+ // It doesn't modify workflow files, so return content unchanged.
+ return content, false, nil
+ },
+ }
+}
+
+// MigrateActionsLockFile moves .github/aw/actions-lock.json to
+// .github/workflows/aw-lock.yml and converts the format from JSON to YAML.
+// Returns (migrated, error): migrated is true when the migration was performed.
+func MigrateActionsLockFile(write bool, verbose bool) (bool, error) {
+ legacyPath := filepath.Join(".github", "aw", workflow.LegacyCacheFileName)
+ newPath := filepath.Join(".github", "workflows", workflow.CacheFileName)
+
+ // Check whether the legacy file exists.
+ if _, err := os.Stat(legacyPath); os.IsNotExist(err) {
+ return false, nil // nothing to migrate
+ }
+
+ if verbose || !write {
+ fmt.Fprintf(os.Stderr, "%s\n", console.FormatInfoMessage(
+ fmt.Sprintf("Found legacy %s – migrating to %s", legacyPath, newPath)))
+ }
+
+ if !write {
+ fmt.Fprintf(os.Stderr, "%s\n", console.FormatInfoMessage(
+ fmt.Sprintf("Would migrate %s to %s", legacyPath, newPath)))
+ return true, nil
+ }
+
+ // If the new file already exists, skip migration to avoid overwriting.
+ if _, err := os.Stat(newPath); err == nil {
+ // Both files exist: warn and remove the legacy file.
+ fmt.Fprintf(os.Stderr, "%s\n", console.FormatWarningMessage(
+ fmt.Sprintf("%s already exists; removing legacy %s", newPath, legacyPath)))
+ if err := os.Remove(legacyPath); err != nil {
+ return false, fmt.Errorf("removing legacy %s: %w", legacyPath, err)
+ }
+ return true, nil
+ }
+
+ // Load via ActionCache (which handles the legacy JSON format) and re-save
+ // to the new YAML path.
+ cache := workflow.NewActionCache(".")
+ if err := cache.Load(); err != nil {
+ return false, fmt.Errorf("loading %s: %w", legacyPath, err)
+ }
+
+ // Force a save even if the cache appears clean (it was loaded from the old path).
+ cache.MarkDirty()
+
+ if err := cache.Save(); err != nil {
+ return false, fmt.Errorf("saving %s: %w", newPath, err)
+ }
+
+ // Remove the old file.
+ if err := os.Remove(legacyPath); err != nil {
+ return false, fmt.Errorf("removing legacy %s: %w", legacyPath, err)
+ }
+
+ fmt.Fprintf(os.Stderr, "%s\n", console.FormatSuccessMessage(
+ fmt.Sprintf("Migrated %s → %s", legacyPath, newPath)))
+ return true, nil
+}
diff --git a/pkg/cli/compile_integration_test.go b/pkg/cli/compile_integration_test.go
index 8b4b891afba..d24ccbf5bb0 100644
--- a/pkg/cli/compile_integration_test.go
+++ b/pkg/cli/compile_integration_test.go
@@ -14,6 +14,7 @@ import (
"github.com/creack/pty"
"github.com/github/gh-aw/pkg/fileutil"
+ "github.com/github/gh-aw/pkg/workflow"
)
// Global binary path shared across all integration tests
@@ -67,7 +68,7 @@ func TestMain(m *testing.M) {
}
// Clean up any action cache files created during tests
- // Tests may create .github/aw/actions-lock.json in the pkg/cli directory
+ // Tests may create .github/workflows/aw-lock.yml in the pkg/cli directory
actionCacheDir := filepath.Join(wd, ".github")
if _, err := os.Stat(actionCacheDir); err == nil {
_ = os.RemoveAll(actionCacheDir)
@@ -1278,22 +1279,22 @@ Test workflow to verify actions-lock.json path handling when compiling from subd
t.Fatalf("Failed to change back to temp directory: %v", err)
}
- // Verify actions-lock.json is created at the repository root (.github/aw/actions-lock.json)
- // NOT at .github/workflows/.github/aw/actions-lock.json
- expectedLockPath := filepath.Join(setup.tempDir, ".github", "aw", "actions-lock.json")
- wrongLockPath := filepath.Join(setup.workflowsDir, ".github", "aw", "actions-lock.json")
+ // Verify aw-lock.yml is created at the repository root (.github/workflows/aw-lock.yml)
+ // NOT at .github/workflows/.github/workflows/aw-lock.yml
+ expectedLockPath := filepath.Join(setup.tempDir, ".github", "workflows", workflow.CacheFileName)
+ wrongLockPath := filepath.Join(setup.workflowsDir, ".github", "workflows", workflow.CacheFileName)
- // Check if actions-lock.json exists (it may or may not, depending on whether actions were pinned)
+ // Check if aw-lock.yml exists (it may or may not, depending on whether actions were pinned)
// The important part is that if it exists, it's in the right place
if _, err := os.Stat(expectedLockPath); err == nil {
- t.Logf("actions-lock.json correctly created at repo root: %s", expectedLockPath)
+ t.Logf("aw-lock.yml correctly created at repo root: %s", expectedLockPath)
} else if !os.IsNotExist(err) {
- t.Fatalf("Failed to check for actions-lock.json at expected path: %v", err)
+ t.Fatalf("Failed to check for aw-lock.yml at expected path: %v", err)
}
- // Verify actions-lock.json was NOT created in the wrong location
+ // Verify aw-lock.yml was NOT created in the wrong location
if _, err := os.Stat(wrongLockPath); err == nil {
- t.Errorf("actions-lock.json incorrectly created at nested path: %s (should be at repo root)", wrongLockPath)
+ t.Errorf("aw-lock.yml incorrectly created at nested path: %s (should be at repo root)", wrongLockPath)
}
// Verify the workflow lock file was created
@@ -1302,7 +1303,7 @@ Test workflow to verify actions-lock.json path handling when compiling from subd
t.Fatalf("Expected lock file %s was not created", lockFilePath)
}
- t.Logf("Integration test passed - actions-lock.json created at correct location")
+ t.Logf("Integration test passed - aw-lock.yml created at correct location")
}
// TestCompileSafeOutputsActions verifies that a workflow with safe-outputs.actions
diff --git a/pkg/cli/fix_codemods.go b/pkg/cli/fix_codemods.go
index 4d509c8e824..6a7b2b9f24f 100644
--- a/pkg/cli/fix_codemods.go
+++ b/pkg/cli/fix_codemods.go
@@ -51,6 +51,7 @@ func GetAllCodemods() []Codemod {
getPluginsToDependenciesCodemod(), // Migrate plugins to dependencies (plugins removed in favour of APM)
getGitHubReposToAllowedReposCodemod(), // Rename deprecated tools.github.repos to tools.github.allowed-repos
getDIFCProxyToIntegrityProxyCodemod(), // Migrate deprecated features.difc-proxy to tools.github.integrity-proxy
+ getActionsLockMigrationCodemod(), // Migrate .github/aw/actions-lock.json to .github/workflows/aw-lock.yml
}
fixCodemodsLog.Printf("Loaded codemod registry: %d codemods available", len(codemods))
return codemods
diff --git a/pkg/cli/fix_command.go b/pkg/cli/fix_command.go
index 3c5cfc0997d..9f358ff92f6 100644
--- a/pkg/cli/fix_command.go
+++ b/pkg/cli/fix_command.go
@@ -50,9 +50,10 @@ The command will:
Without --write (dry-run mode), no files are modified. With --write, the command performs
all steps and additionally:
4. Write updated files back to disk
- 5. Delete deprecated .github/aw/schemas/agentic-workflow.json file if it exists
- 6. Delete old template files from previous versions if present
- 7. Delete old workflow-specific .agent.md files from .github/agents/ if present
+ 5. Migrate .github/aw/actions-lock.json to .github/workflows/aw-lock.yml if it exists
+ 6. Delete deprecated .github/aw/schemas/agentic-workflow.json file if it exists
+ 7. Delete old template files from previous versions if present
+ 8. Delete old workflow-specific .agent.md files from .github/agents/ if present
` + WorkflowIDExplanation + `
@@ -205,6 +206,13 @@ func runFixCommand(workflowIDs []string, write bool, verbose bool, workflowDir s
}
}
+ // Migrate legacy .github/aw/actions-lock.json → .github/workflows/aw-lock.yml
+ fixLog.Print("Checking for legacy actions-lock.json to migrate")
+ if _, err := MigrateActionsLockFile(write, verbose); err != nil {
+ fixLog.Printf("Failed to migrate actions-lock.json: %v", err)
+ fmt.Fprintf(os.Stderr, "%s\n", console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to migrate actions-lock.json: %v", err)))
+ }
+
// Delete deprecated schema file if it exists
schemaPath := filepath.Join(".github", "aw", "schemas", "agentic-workflow.json")
if _, err := os.Stat(schemaPath); err == nil {
diff --git a/pkg/cli/update_actions.go b/pkg/cli/update_actions.go
index 80ac06eb553..763a0e972eb 100644
--- a/pkg/cli/update_actions.go
+++ b/pkg/cli/update_actions.go
@@ -22,7 +22,7 @@ func isCoreAction(repo string) bool {
return strings.HasPrefix(repo, "actions/")
}
-// UpdateActions updates GitHub Actions versions in .github/aw/actions-lock.json
+// UpdateActions updates GitHub Actions versions in .github/workflows/aw-lock.yml
// It checks each action for newer releases and updates the SHA if a newer version is found.
// By default all actions are updated to the latest major version; pass disableReleaseBump=true
// to revert to the old behaviour where only core (actions/*) actions bypass the --major flag.
@@ -37,14 +37,18 @@ func UpdateActions(allowMajor, verbose, disableReleaseBump bool) error {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Checking for GitHub Actions updates..."))
}
- // Load the action cache (actions-lock.json) using the shared ActionCache helpers
+ // Load the action cache (aw-lock.yml) using the shared ActionCache helpers
// so that cached inputs/descriptions for safe-outputs.actions entries are preserved.
- actionsLockPath := filepath.Join(".github", "aw", "actions-lock.json")
- if _, err := os.Stat(actionsLockPath); os.IsNotExist(err) {
- if verbose {
- fmt.Fprintln(os.Stderr, console.FormatVerboseMessage("Actions lock file not found: "+actionsLockPath))
+ // NewActionCache also handles loading from the legacy actions-lock.json path.
+ awLockPath := filepath.Join(".github", "workflows", workflow.CacheFileName)
+ legacyPath := filepath.Join(".github", "aw", workflow.LegacyCacheFileName)
+ if _, err := os.Stat(awLockPath); os.IsNotExist(err) {
+ if _, err2 := os.Stat(legacyPath); os.IsNotExist(err2) {
+ if verbose {
+ fmt.Fprintln(os.Stderr, console.FormatVerboseMessage("Actions lock file not found: "+awLockPath))
+ }
+ return nil // Not an error, just skip
}
- return nil // Not an error, just skip
}
actionCache := workflow.NewActionCache(".")
@@ -52,7 +56,7 @@ func UpdateActions(allowMajor, verbose, disableReleaseBump bool) error {
return fmt.Errorf("failed to parse actions lock file: %w", err)
}
- updateLog.Printf("Loaded %d action entries from actions-lock.json", len(actionCache.Entries))
+ updateLog.Printf("Loaded %d action entries from aw-lock.yml", len(actionCache.Entries))
// Track updates
var updatedActions []string
@@ -153,8 +157,8 @@ func UpdateActions(allowMajor, verbose, disableReleaseBump bool) error {
return fmt.Errorf("failed to save actions lock file: %w", err)
}
- updateLog.Printf("Successfully wrote updated actions-lock.json with %d updates", len(updatedActions))
- fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Updated actions-lock.json file"))
+ updateLog.Printf("Successfully wrote updated aw-lock.yml with %d updates", len(updatedActions))
+ fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Updated aw-lock.yml file"))
}
return nil
diff --git a/pkg/cli/update_command.go b/pkg/cli/update_command.go
index 9abc5f6c151..b938dcf2904 100644
--- a/pkg/cli/update_command.go
+++ b/pkg/cli/update_command.go
@@ -118,13 +118,13 @@ func RunUpdateWorkflows(workflowNames []string, allowMajor, force, verbose bool,
firstErr = fmt.Errorf("workflow update failed: %w", err)
}
- // Update GitHub Actions versions in actions-lock.json.
+ // Update GitHub Actions versions in aw-lock.yml.
// By default all actions are updated to the latest major version.
// Pass --disable-release-bump to revert to only forcing updates for core (actions/*) actions.
- updateLog.Printf("Updating GitHub Actions versions in actions-lock.json: allowMajor=%v, disableReleaseBump=%v", allowMajor, disableReleaseBump)
+ updateLog.Printf("Updating GitHub Actions versions in aw-lock.yml: allowMajor=%v, disableReleaseBump=%v", allowMajor, disableReleaseBump)
if err := UpdateActions(allowMajor, verbose, disableReleaseBump); err != nil {
// Non-fatal: warn but don't fail the update
- fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to update actions-lock.json: %v", err)))
+ fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to update aw-lock.yml: %v", err)))
}
// Update action references in user-provided steps within workflow .md files.
diff --git a/pkg/cli/update_command_test.go b/pkg/cli/update_command_test.go
index bc48b591164..c0b08e0260e 100644
--- a/pkg/cli/update_command_test.go
+++ b/pkg/cli/update_command_test.go
@@ -738,7 +738,7 @@ func TestMarshalActionsLockSorted(t *testing.T) {
}
// Read the file back
- data, err := os.ReadFile(filepath.Join(tmpDir, ".github", "aw", "actions-lock.json"))
+ data, err := os.ReadFile(filepath.Join(tmpDir, ".github", "workflows", workflow.CacheFileName))
if err != nil {
t.Fatalf("Expected to read saved file, got: %v", err)
}
@@ -757,20 +757,20 @@ func TestMarshalActionsLockSorted(t *testing.T) {
t.Errorf("Expected actions/checkout to appear before zebra/action in sorted output")
}
- // Check JSON structure
- if !strings.Contains(result, `"entries": {`) {
- t.Error("Expected 'entries' key in JSON output")
+ // Check YAML structure
+ if !strings.Contains(result, "actions:") {
+ t.Error("Expected 'actions:' key in YAML output")
}
- if !strings.Contains(result, `"repo": "actions/checkout"`) {
+ if !strings.Contains(result, "repo: actions/checkout") {
t.Error("Expected actions/checkout repo in output")
}
- if !strings.Contains(result, `"version": "v5"`) {
+ if !strings.Contains(result, "version: v5") {
t.Error("Expected v5 version in output")
}
- if !strings.Contains(result, `"sha": "def456"`) {
+ if !strings.Contains(result, "sha: def456") {
t.Error("Expected def456 SHA in output")
}
}
diff --git a/pkg/cli/upgrade_command.go b/pkg/cli/upgrade_command.go
index db6530d876a..a0ce2dd1728 100644
--- a/pkg/cli/upgrade_command.go
+++ b/pkg/cli/upgrade_command.go
@@ -38,7 +38,7 @@ func NewUpgradeCommand() *cobra.Command {
This command:
1. Updates the dispatcher agent file to the latest template (like 'init' command)
2. Applies automatic codemods to fix deprecated fields in all workflows (like 'fix --write')
- 3. Updates GitHub Actions versions in .github/aw/actions-lock.json (unless --no-actions is set)
+ 3. Updates GitHub Actions versions in .github/workflows/aw-lock.yml (unless --no-actions is set)
4. Compiles all workflows to generate lock files (like 'compile' command)
DEPENDENCY HEALTH AUDIT:
diff --git a/pkg/workflow/.github/aw/actions-lock.json b/pkg/workflow/.github/aw/actions-lock.json
deleted file mode 100644
index 122faf0f798..00000000000
--- a/pkg/workflow/.github/aw/actions-lock.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "entries": {
- "actions/ai-inference@v1": {
- "repo": "actions/ai-inference",
- "version": "v1",
- "sha": "b81b2afb8390ee6839b494a404766bef6493c7d9"
- },
- "actions/checkout@v4": {
- "repo": "actions/checkout",
- "version": "v4",
- "sha": "08eba0b27e820071cde6df949e0beb9ba4906955"
- },
- "actions/checkout@v5": {
- "repo": "actions/checkout",
- "version": "v5",
- "sha": "93cb6efe18208431cddfb8368fd83d5badbf9bfd"
- },
- "actions/checkout@v6": {
- "repo": "actions/checkout",
- "version": "v6.0.2",
- "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd"
- },
- "actions/setup-node@v4": {
- "repo": "actions/setup-node",
- "version": "v4",
- "sha": "49933ea5288caeca8642d1e84afbd3f7d6820020"
- },
- "actions/setup-node@v6": {
- "repo": "actions/setup-node",
- "version": "v6",
- "sha": "2028fbc5c25fe9cf00d9f06a71cc4710d4507903"
- }
- }
-}
diff --git a/pkg/workflow/.github/workflows/aw-lock.yml b/pkg/workflow/.github/workflows/aw-lock.yml
new file mode 100644
index 00000000000..f4a4b1f8f96
--- /dev/null
+++ b/pkg/workflow/.github/workflows/aw-lock.yml
@@ -0,0 +1,27 @@
+version: "1"
+actions:
+ 'actions/ai-inference@v1':
+ repo: actions/ai-inference
+ version: v1
+ sha: b81b2afb8390ee6839b494a404766bef6493c7d9
+ 'actions/checkout@v4':
+ repo: actions/checkout
+ version: v4
+ sha: 08eba0b27e820071cde6df949e0beb9ba4906955
+ 'actions/checkout@v5':
+ repo: actions/checkout
+ version: v5
+ sha: 93cb6efe18208431cddfb8368fd83d5badbf9bfd
+ 'actions/checkout@v6':
+ repo: actions/checkout
+ version: v6.0.2
+ sha: de0fac2e4500dabe0009e67214ff5f5447ce83dd
+ 'actions/setup-node@v4':
+ repo: actions/setup-node
+ version: v4
+ sha: 49933ea5288caeca8642d1e84afbd3f7d6820020
+ 'actions/setup-node@v6':
+ repo: actions/setup-node
+ version: v6
+ sha: 2028fbc5c25fe9cf00d9f06a71cc4710d4507903
+
diff --git a/pkg/workflow/action_cache.go b/pkg/workflow/action_cache.go
index 48ed232e494..cde0af3306f 100644
--- a/pkg/workflow/action_cache.go
+++ b/pkg/workflow/action_cache.go
@@ -9,72 +9,146 @@ import (
"strings"
"github.com/github/gh-aw/pkg/logger"
+ "go.yaml.in/yaml/v3"
)
var actionCacheLog = logger.New("workflow:action_cache")
const (
- // CacheFileName is the name of the cache file in .github/aw/.
- CacheFileName = "actions-lock.json"
+ // CacheFileName is the name of the lock file in .github/workflows/.
+ CacheFileName = "aw-lock.yml"
+
+ // LegacyCacheFileName is the old name of the lock file in .github/aw/.
+ // Used for backward-compatible loading and migration codemods.
+ LegacyCacheFileName = "actions-lock.json"
+
+ // awLockFileVersion is the current version of the aw-lock.yml format.
+ awLockFileVersion = "1"
)
// ActionCacheEntry represents a cached action pin resolution.
type ActionCacheEntry struct {
- Repo string `json:"repo"`
- Version string `json:"version"`
- SHA string `json:"sha"`
- Inputs map[string]*ActionYAMLInput `json:"inputs,omitempty"` // cached inputs from action.yml
- ActionDescription string `json:"action_description,omitempty"` // cached description from action.yml
+ Repo string `json:"repo" yaml:"repo"`
+ Version string `json:"version" yaml:"version"`
+ SHA string `json:"sha" yaml:"sha"`
+ Inputs map[string]*ActionYAMLInput `json:"inputs,omitempty" yaml:"inputs,omitempty"` // cached inputs from action.yml
+ ActionDescription string `json:"action_description,omitempty" yaml:"action_description,omitempty"` // cached description from action.yml
+}
+
+// ContainerPinEntry represents a cached container image pin resolution.
+type ContainerPinEntry struct {
+ Image string `yaml:"image"`
+ Digest string `yaml:"digest"`
+}
+
+// awLockFileFormat is the on-disk representation of aw-lock.yml.
+type awLockFileFormat struct {
+ Version string `yaml:"version"`
+ Actions map[string]ActionCacheEntry `yaml:"actions"`
+ Containers map[string]ContainerPinEntry `yaml:"containers,omitempty"`
+}
+
+// legacyActionsLockFormat is the on-disk representation of the old actions-lock.json.
+type legacyActionsLockFormat struct {
+ Entries map[string]ActionCacheEntry `json:"entries"`
}
// ActionCache manages cached action pin resolutions.
type ActionCache struct {
- Entries map[string]ActionCacheEntry `json:"entries"` // key: "repo@version"
- path string
- dirty bool // tracks if cache has unsaved changes
+ Entries map[string]ActionCacheEntry // key: "repo@version"
+ Containers map[string]ContainerPinEntry // key: image name
+ path string
+ dirty bool // tracks if cache has unsaved changes
}
// NewActionCache creates a new action cache instance
func NewActionCache(repoRoot string) *ActionCache {
- cachePath := filepath.Join(repoRoot, ".github", "aw", CacheFileName)
+ cachePath := filepath.Join(repoRoot, ".github", "workflows", CacheFileName)
actionCacheLog.Printf("Creating action cache with path: %s", cachePath)
return &ActionCache{
- Entries: make(map[string]ActionCacheEntry),
- path: cachePath,
+ Entries: make(map[string]ActionCacheEntry),
+ Containers: make(map[string]ContainerPinEntry),
+ path: cachePath,
// dirty is initialized to false (zero value)
}
}
-// Load loads the cache from disk
+// Load loads the cache from disk.
+// It first tries the new YAML format at .github/workflows/aw-lock.yml.
+// If that file does not exist, it falls back to the legacy JSON format at
+// .github/aw/actions-lock.json for backward compatibility.
func (c *ActionCache) Load() error {
actionCacheLog.Printf("Loading action cache from: %s", c.path)
data, err := os.ReadFile(c.path)
if err != nil {
- if os.IsNotExist(err) {
- // Cache file doesn't exist yet, that's OK
- actionCacheLog.Print("Cache file does not exist, starting with empty cache")
- return nil
+ if !os.IsNotExist(err) {
+ actionCacheLog.Printf("Failed to read cache file: %v", err)
+ return err
}
- actionCacheLog.Printf("Failed to read cache file: %v", err)
- return err
+ // New path doesn't exist — try legacy path for backward compatibility.
+ legacyPath := legacyCachePath(c.path)
+ actionCacheLog.Printf("Cache file not found; trying legacy path: %s", legacyPath)
+ data, err = os.ReadFile(legacyPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ actionCacheLog.Print("Cache file does not exist, starting with empty cache")
+ return nil
+ }
+ actionCacheLog.Printf("Failed to read legacy cache file: %v", err)
+ return err
+ }
+ actionCacheLog.Printf("Loaded from legacy cache path: %s", legacyPath)
+ return c.loadLegacyJSON(data)
}
- if err := json.Unmarshal(data, c); err != nil {
- actionCacheLog.Printf("Failed to unmarshal cache data: %v", err)
+ var lf awLockFileFormat
+ if err := yaml.Unmarshal(data, &lf); err != nil {
+ actionCacheLog.Printf("Failed to unmarshal YAML cache data: %v", err)
return err
}
+ if lf.Actions != nil {
+ c.Entries = lf.Actions
+ }
+ if lf.Containers != nil {
+ c.Containers = lf.Containers
+ }
+
// Mark cache as clean after successful load (it matches disk state)
c.dirty = false
- actionCacheLog.Printf("Successfully loaded cache with %d entries", len(c.Entries))
+ actionCacheLog.Printf("Successfully loaded cache with %d action entries", len(c.Entries))
+ return nil
+}
+
+// loadLegacyJSON populates the cache from the old actions-lock.json format.
+func (c *ActionCache) loadLegacyJSON(data []byte) error {
+ var legacy legacyActionsLockFormat
+ if err := json.Unmarshal(data, &legacy); err != nil {
+ actionCacheLog.Printf("Failed to unmarshal legacy JSON cache data: %v", err)
+ return err
+ }
+ if legacy.Entries != nil {
+ c.Entries = legacy.Entries
+ }
+ c.dirty = false
+ actionCacheLog.Printf("Successfully loaded legacy cache with %d entries", len(c.Entries))
return nil
}
-// Save saves the cache to disk with sorted entries
-// If the cache is empty, the file is not created or is deleted if it exists
-// Deduplicates entries by keeping only the most precise version reference for each repo+SHA combination
-// Only saves if the cache has been modified (dirty flag is true)
+// legacyCachePath derives the old .github/aw/actions-lock.json path from
+// the new .github/workflows/aw-lock.yml path.
+func legacyCachePath(newPath string) string {
+ dir := filepath.Dir(newPath) // .github/workflows
+ repoRoot := filepath.Dir(dir) // .github
+ repoRoot = filepath.Dir(repoRoot) // repo root
+ return filepath.Join(repoRoot, ".github", "aw", LegacyCacheFileName)
+}
+
+// Save saves the cache to disk with sorted entries.
+// If the cache is empty, the file is not created or is deleted if it exists.
+// Deduplicates entries by keeping only the most precise version reference for each repo+SHA combination.
+// Only saves if the cache has been modified (dirty flag is true).
func (c *ActionCache) Save() error {
// Skip saving if cache hasn't been modified
if !c.dirty {
@@ -85,7 +159,7 @@ func (c *ActionCache) Save() error {
actionCacheLog.Printf("Saving action cache to: %s with %d entries", c.path, len(c.Entries))
// If cache is empty, skip saving and delete the file if it exists
- if len(c.Entries) == 0 {
+ if len(c.Entries) == 0 && len(c.Containers) == 0 {
actionCacheLog.Print("Cache is empty, skipping file creation")
// Remove the file if it exists
if _, err := os.Stat(c.path); err == nil {
@@ -109,14 +183,14 @@ func (c *ActionCache) Save() error {
return err
}
- // Marshal with sorted entries
- data, err := c.marshalSorted()
+ // Marshal with sorted entries in YAML format
+ data, err := c.marshalSortedYAML()
if err != nil {
actionCacheLog.Printf("Failed to marshal cache data: %v", err)
return err
}
- // Add trailing newline for prettier compliance
+ // Add trailing newline
data = append(data, '\n')
if err := os.WriteFile(c.path, data, 0644); err != nil {
@@ -129,41 +203,81 @@ func (c *ActionCache) Save() error {
return nil
}
-// marshalSorted marshals the cache with entries sorted by key
-func (c *ActionCache) marshalSorted() ([]byte, error) {
- // Extract and sort the keys
- keys := make([]string, 0, len(c.Entries))
+// marshalSortedYAML marshals the cache as YAML with sorted action entries.
+func (c *ActionCache) marshalSortedYAML() ([]byte, error) {
+ // Sort action keys
+ actionKeys := make([]string, 0, len(c.Entries))
for key := range c.Entries {
- keys = append(keys, key)
+ actionKeys = append(actionKeys, key)
}
- sort.Strings(keys)
+ sort.Strings(actionKeys)
- // Manually construct JSON with sorted keys
- var result []byte
- result = append(result, []byte("{\n \"entries\": {\n")...)
+ var sb strings.Builder
+ sb.WriteString("version: \"")
+ sb.WriteString(awLockFileVersion)
+ sb.WriteString("\"\n")
+ sb.WriteString("actions:\n")
- for i, key := range keys {
+ for _, key := range actionKeys {
entry := c.Entries[key]
- // Marshal the entry
- entryJSON, err := json.MarshalIndent(entry, " ", " ")
+ // Marshal the entry fields as YAML.
+ entryBytes, err := yaml.Marshal(entry)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("marshaling action entry %q: %w", key, err)
}
- // Add the key and entry
- result = append(result, []byte(" \""+key+"\": ")...)
- result = append(result, entryJSON...)
+ // Write the key (indent 2 spaces, quote if needed for YAML safety).
+ sb.WriteString(" ")
+ sb.WriteString(yamlQuoteKey(key))
+ sb.WriteString(":\n")
+
+ // Indent each line of the entry YAML by 4 spaces.
+ for line := range strings.SplitSeq(strings.TrimRight(string(entryBytes), "\n"), "\n") {
+ sb.WriteString(" ")
+ sb.WriteString(line)
+ sb.WriteString("\n")
+ }
+ }
- // Add comma if not the last entry
- if i < len(keys)-1 {
- result = append(result, ',')
+ // Write containers section.
+ if len(c.Containers) > 0 {
+ containerKeys := make([]string, 0, len(c.Containers))
+ for key := range c.Containers {
+ containerKeys = append(containerKeys, key)
+ }
+ sort.Strings(containerKeys)
+
+ sb.WriteString("containers:\n")
+ for _, key := range containerKeys {
+ entry := c.Containers[key]
+ entryBytes, err := yaml.Marshal(entry)
+ if err != nil {
+ return nil, fmt.Errorf("marshaling container entry %q: %w", key, err)
+ }
+ sb.WriteString(" ")
+ sb.WriteString(yamlQuoteKey(key))
+ sb.WriteString(":\n")
+ for line := range strings.SplitSeq(strings.TrimRight(string(entryBytes), "\n"), "\n") {
+ sb.WriteString(" ")
+ sb.WriteString(line)
+ sb.WriteString("\n")
+ }
}
- result = append(result, '\n')
}
- result = append(result, []byte(" }\n}")...)
- return result, nil
+ return []byte(sb.String()), nil
+}
+
+// yamlQuoteKey returns a YAML-safe key string, quoting it if it contains
+// characters that require quoting (e.g. '@', '/', ':').
+func yamlQuoteKey(key string) string {
+ needsQuoting := strings.ContainsAny(key, "@/:[]{}#&*?|<>=!%,`\"'\\")
+ if needsQuoting {
+ escaped := strings.ReplaceAll(key, "'", "''")
+ return "'" + escaped + "'"
+ }
+ return key
}
// Delete removes the cache entry for the given repo and version.
@@ -356,6 +470,14 @@ func (c *ActionCache) GetCachePath() string {
return c.path
}
+// MarkDirty forces the cache to be saved on the next call to Save,
+// even if no entries have changed via Set/Delete. This is useful when
+// the cache was loaded from a legacy format and needs to be written to the
+// new location/format.
+func (c *ActionCache) MarkDirty() {
+ c.dirty = true
+}
+
// deduplicateEntries removes duplicate entries by keeping only the most precise version reference
// for each repo+SHA combination. For example, if both "actions/cache@v4" and "actions/cache@v4.3.0"
// point to the same SHA and version, only "actions/cache@v4.3.0" is kept.
diff --git a/pkg/workflow/action_cache_test.go b/pkg/workflow/action_cache_test.go
index f5bf5acb588..7d3ba193020 100644
--- a/pkg/workflow/action_cache_test.go
+++ b/pkg/workflow/action_cache_test.go
@@ -3,12 +3,12 @@
package workflow
import (
- "encoding/json"
"os"
"path/filepath"
"testing"
"github.com/github/gh-aw/pkg/testutil"
+ "go.yaml.in/yaml/v3"
)
func TestActionCache(t *testing.T) {
@@ -51,7 +51,7 @@ func TestActionCacheSaveLoad(t *testing.T) {
}
// Verify file exists
- cachePath := filepath.Join(tmpDir, ".github", "aw", CacheFileName)
+ cachePath := filepath.Join(tmpDir, ".github", "workflows", CacheFileName)
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
t.Fatalf("Cache file was not created at %s", cachePath)
}
@@ -97,7 +97,7 @@ func TestActionCacheGetCachePath(t *testing.T) {
tmpDir := testutil.TempDir(t, "test-*")
cache := NewActionCache(tmpDir)
- expectedPath := filepath.Join(tmpDir, ".github", "aw", CacheFileName)
+ expectedPath := filepath.Join(tmpDir, ".github", "workflows", CacheFileName)
if cache.GetCachePath() != expectedPath {
t.Errorf("Expected cache path '%s', got '%s'", expectedPath, cache.GetCachePath())
}
@@ -118,7 +118,7 @@ func TestActionCacheTrailingNewline(t *testing.T) {
}
// Read the file and check for trailing newline
- cachePath := filepath.Join(tmpDir, ".github", "aw", CacheFileName)
+ cachePath := filepath.Join(tmpDir, ".github", "workflows", CacheFileName)
data, err := os.ReadFile(cachePath)
if err != nil {
t.Fatalf("Failed to read cache file: %v", err)
@@ -149,7 +149,7 @@ func TestActionCacheSortedEntries(t *testing.T) {
}
// Read the file content
- cachePath := filepath.Join(tmpDir, ".github", "aw", CacheFileName)
+ cachePath := filepath.Join(tmpDir, ".github", "workflows", CacheFileName)
data, err := os.ReadFile(cachePath)
if err != nil {
t.Fatalf("Failed to read cache file: %v", err)
@@ -179,16 +179,16 @@ func TestActionCacheSortedEntries(t *testing.T) {
lastPos = pos
}
- // Also verify the file is valid JSON
- var loadedCache ActionCache
- err = json.Unmarshal(data, &loadedCache)
+ // Also verify the file is valid YAML
+ var loadedFile awLockFileFormat
+ err = yaml.Unmarshal(data, &loadedFile)
if err != nil {
- t.Fatalf("Saved cache is not valid JSON: %v", err)
+ t.Fatalf("Saved cache is not valid YAML: %v", err)
}
// Verify all entries are present
- if len(loadedCache.Entries) != 5 {
- t.Errorf("Expected 5 entries, got %d", len(loadedCache.Entries))
+ if len(loadedFile.Actions) != 5 {
+ t.Errorf("Expected 5 entries, got %d", len(loadedFile.Actions))
}
}
@@ -216,7 +216,7 @@ func TestActionCacheEmptySaveDoesNotCreateFile(t *testing.T) {
}
// Verify file does NOT exist
- cachePath := filepath.Join(tmpDir, ".github", "aw", CacheFileName)
+ cachePath := filepath.Join(tmpDir, ".github", "workflows", CacheFileName)
if _, err := os.Stat(cachePath); !os.IsNotExist(err) {
t.Error("Empty cache should not create a file")
}
@@ -235,7 +235,7 @@ func TestActionCacheEmptySaveDeletesExistingFile(t *testing.T) {
}
// Verify file exists
- cachePath := filepath.Join(tmpDir, ".github", "aw", CacheFileName)
+ cachePath := filepath.Join(tmpDir, ".github", "workflows", CacheFileName)
if _, err := os.Stat(cachePath); os.IsNotExist(err) {
t.Fatal("Cache file should exist after saving with entries")
}
diff --git a/pkg/workflow/action_sha_validation_test.go b/pkg/workflow/action_sha_validation_test.go
index 7922187f444..ab385dc71bb 100644
--- a/pkg/workflow/action_sha_validation_test.go
+++ b/pkg/workflow/action_sha_validation_test.go
@@ -221,7 +221,7 @@ jobs:
cache.Set("actions/checkout", "v5", "93cb6efe18208431cddfb8368fd83d5badbf9bfd")
// Verify cache file doesn't exist before validation
- cachePath := filepath.Join(testDir, ".github", "aw", CacheFileName)
+ cachePath := filepath.Join(testDir, ".github", "workflows", CacheFileName)
if _, err := os.Stat(cachePath); !os.IsNotExist(err) {
os.RemoveAll(filepath.Join(testDir, ".github"))
}
diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go
index 91fa861f4ab..4a5a5be180b 100644
--- a/pkg/workflow/compiler_types.go
+++ b/pkg/workflow/compiler_types.go
@@ -96,7 +96,7 @@ func NewCompiler(opts ...CompilerOption) *Compiler {
version := GetVersion()
// Auto-detect git repository root for action cache path resolution
- // This ensures actions-lock.json is created at repo root regardless of CWD
+ // This ensures aw-lock.yml is created at repo root regardless of CWD
gitRoot := findGitRoot()
// Create compiler with defaults
diff --git a/pkg/workflow/data/action_pins.json b/pkg/workflow/data/action_pins.json
index 4432457f3b3..8a2e1b71801 100644
--- a/pkg/workflow/data/action_pins.json
+++ b/pkg/workflow/data/action_pins.json
@@ -20,8 +20,7 @@
"description": "The owner and repository name. e.g.) Codertocat/Hello-World",
"default": "${{ github.repository }}"
}
- },
- "action_description": "Add labels to an issue or a pull request."
+ }
},
"actions/ai-inference@v2.0.8": {
"repo": "actions/ai-inference",
diff --git a/pkg/workflow/safe_outputs_actions.go b/pkg/workflow/safe_outputs_actions.go
index ab9cf21a894..572208c5f0f 100644
--- a/pkg/workflow/safe_outputs_actions.go
+++ b/pkg/workflow/safe_outputs_actions.go
@@ -166,7 +166,7 @@ func parseActionUsesField(uses string) (*actionRef, error) {
//
// Resolution priority (highest wins):
// 1. Inputs already specified in the frontmatter (config.Inputs != nil)
-// 2. Inputs cached in the ActionCache (actions-lock.json)
+// 2. Inputs cached in the ActionCache (aw-lock.yml)
// 3. Inputs fetched from the remote action.yml (result cached for future runs)
//
// When available, the action reference is pinned to a commit SHA for security;
@@ -220,7 +220,7 @@ func (c *Compiler) fetchAndParseActionYAML(actionName string, config *SafeOutput
if !inputsFromFrontmatter {
// Check the ActionCache for previously-fetched inputs before going to the network.
// The cache key uses the original version tag from the `uses:` field (ref.Ref, e.g.
- // "v1") which matches the key stored in actions-lock.json.
+ // "v1") which matches the key stored in aw-lock.yml.
if data.ActionCache != nil {
if cachedInputs, ok := data.ActionCache.GetInputs(ref.Repo, ref.Ref); ok {
safeOutputActionsLog.Printf("Using cached inputs for %q (%s@%s)", actionName, ref.Repo, ref.Ref)
diff --git a/scratchpad/debugging-action-pinning.md b/scratchpad/debugging-action-pinning.md
index 4574b2b8b6c..742a7f5bda2 100644
--- a/scratchpad/debugging-action-pinning.md
+++ b/scratchpad/debugging-action-pinning.md
@@ -69,17 +69,17 @@ DEBUG=workflow:action_* gh aw compile 2> debug.log
### 2. Inspect the Action Cache
-The action cache is stored at `.github/aw/actions-lock.json`. Examine it for duplicate entries:
+The action cache is stored at `.github/workflows/aw-lock.yml`. Examine it for duplicate entries:
```bash
# Pretty-print the cache
-cat .github/aw/actions-lock.json | jq .
+cat .github/workflows/aw-lock.yml | jq .
# Find entries for a specific action
-cat .github/aw/actions-lock.json | jq '.entries | to_entries[] | select(.value.repo == "actions/github-script")'
+cat .github/workflows/aw-lock.yml | jq '.entries | to_entries[] | select(.value.repo == "actions/github-script")'
# Check for duplicate SHAs
-cat .github/aw/actions-lock.json | jq -r '.entries | to_entries[] | "\(.value.sha) \(.key)"' | sort | uniq -d -w 40
+cat .github/workflows/aw-lock.yml | jq -r '.entries | to_entries[] | "\(.value.sha) \(.key)"' | sort | uniq -d -w 40
```
### 3. Check for Version Aliases
@@ -120,7 +120,7 @@ workflow:action_pins Dynamic resolution succeeded: actions/github-script@v8 →
#### Cache Operations
```
-workflow:action_cache Loading action cache from: .github/aw/actions-lock.json
+workflow:action_cache Loading action cache from: .github/workflows/aw-lock.yml
workflow:action_cache Successfully loaded cache with 15 entries
workflow:action_cache Setting cache entry: key=actions/github-script@v8, sha=ed597411...
workflow:action_cache Deduplicating: keeping actions/github-script@v8.0.0, removing actions/github-script@v8
@@ -176,7 +176,7 @@ If you suspect cache corruption, clear it and recompile:
```bash
# Remove the cache file
-rm .github/aw/actions-lock.json
+rm .github/workflows/aw-lock.yml
# Recompile all workflows
gh aw compile
@@ -223,7 +223,7 @@ If you're using shared workflows that reference actions differently than your lo
CI environments may use a fresh cache on each run, while local development persists the cache:
1. CI may show different version comments than local
-2. Solution: Commit `.github/aw/actions-lock.json` to version control
+2. Solution: Commit `.github/workflows/aw-lock.yml` to version control
3. This ensures consistent resolution across environments
### Scenario 3: Upstream Version Changes
@@ -296,10 +296,10 @@ All workflow files must use full semantic versioning for actions:
### 2. Commit the Action Cache
-Add `.github/aw/actions-lock.json` to version control:
+Add `.github/workflows/aw-lock.yml` to version control:
```bash
-git add .github/aw/actions-lock.json
+git add .github/workflows/aw-lock.yml
git commit -m "chore: add action cache for consistent pinning"
```
@@ -311,9 +311,9 @@ Add to your maintenance workflow:
```bash
# Monthly: clear and regenerate cache
-rm .github/aw/actions-lock.json
+rm .github/workflows/aw-lock.yml
gh aw compile
-git add .github/aw/actions-lock.json
+git add .github/workflows/aw-lock.yml
git commit -m "chore: refresh action cache"
```
@@ -356,13 +356,13 @@ Track cache evolution across compiles:
```bash
# Before
-cp .github/aw/actions-lock.json before.json
+cp .github/workflows/aw-lock.yml before.json
# Compile
gh aw compile
# After
-cp .github/aw/actions-lock.json after.json
+cp .github/workflows/aw-lock.yml after.json
# Compare
diff -u before.json after.json
@@ -374,10 +374,10 @@ Check for unexpected cache entries:
```bash
# List all SHAs with their version tags
-jq -r '.entries | to_entries[] | "\(.value.sha) \(.value.version) \(.key)"' .github/aw/actions-lock.json | sort
+jq -r '.entries | to_entries[] | "\(.value.sha) \(.value.version) \(.key)"' .github/workflows/aw-lock.yml | sort
# Find duplicate SHAs
-jq -r '.entries | to_entries[] | .value.sha' .github/aw/actions-lock.json | sort | uniq -d
+jq -r '.entries | to_entries[] | .value.sha' .github/workflows/aw-lock.yml | sort | uniq -d
```
## Related Documentation
@@ -389,10 +389,10 @@ jq -r '.entries | to_entries[] | .value.sha' .github/aw/actions-lock.json | sort
## Troubleshooting Checklist
- [ ] Enabled debug logging: `DEBUG=workflow:action_* gh aw compile`
-- [ ] Checked `.github/aw/actions-lock.json` for duplicate entries
+- [ ] Checked `.github/workflows/aw-lock.yml` for duplicate entries
- [ ] Verified version tags point to same SHA via GitHub API
- [ ] Searched workflows for inconsistent version formats
-- [ ] Cleared cache and recompiled: `rm .github/aw/actions-lock.json && gh aw compile`
+- [ ] Cleared cache and recompiled: `rm .github/workflows/aw-lock.yml && gh aw compile`
- [ ] Checked for upstream version tag changes
- [ ] Reviewed action_pins.json for canonical versions
- [ ] Consulted team on preferred version format
@@ -404,7 +404,7 @@ jq -r '.entries | to_entries[] | .value.sha' .github/aw/actions-lock.json | sort
If the issue persists after following this guide:
1. Capture debug logs: `DEBUG=workflow:action_* gh aw compile 2> debug.log`
-2. Export cache state: `cat .github/aw/actions-lock.json > cache.json`
+2. Export cache state: `cat .github/workflows/aw-lock.yml > cache.json`
3. List workflow action references: `grep -r "uses: " .github/workflows/ > actions.txt`
4. Create an issue with these artifacts attached
diff --git a/scratchpad/layout.md b/scratchpad/layout.md
index ccde7cd89f2..1a4d57fe126 100644
--- a/scratchpad/layout.md
+++ b/scratchpad/layout.md
@@ -121,7 +121,7 @@ Common file paths referenced in workflow files:
| Path | Type | Description | Usage Context |
|------|------|-------------|---------------|
| `.github/workflows/` | Directory | Workflow definition directory | Contains all `.md` and `.lock.yml` workflow files |
-| `.github/aw/` | Directory | Agentic workflow configuration | Contains `actions-lock.json` and other configs |
+| `.github/aw/` | Directory | Agentic workflow configuration | Contains `aw-lock.yml` and other configs |
| `.github/agents/` | Directory | Custom agent definitions | Contains agent markdown files (e.g., `test-agent.md`) |
| `/tmp/gh-aw/` | Directory | Temporary workflow data | Root temporary directory for all workflow artifacts |
| `/tmp/gh-aw/agent/` | Directory | Agent execution workspace | Agent's working directory during execution |
@@ -382,7 +382,7 @@ GitHub Actions runner images used across compiled workflows:
├── agents/ # Custom agent definitions
│ └── test-agent.md
├── aw/ # Workflow configuration
-│ └── actions-lock.json
+│ └── aw-lock.yml
└── workflows/ # Workflow files
├── *.md # Source workflows
├── *.lock.yml # Compiled workflows
diff --git a/scripts/sync-action-pins/main.go b/scripts/sync-action-pins/main.go
new file mode 100644
index 00000000000..d1109552c2a
--- /dev/null
+++ b/scripts/sync-action-pins/main.go
@@ -0,0 +1,61 @@
+// sync-action-pins converts .github/workflows/aw-lock.yml into
+// pkg/workflow/data/action_pins.json (the embedded JSON fallback used by the
+// compiler for SHA pinning when the GitHub API is unavailable).
+//
+// It also supports the legacy source path .github/aw/actions-lock.json for
+// repositories that have not yet migrated.
+//
+// Usage: go run ./scripts/sync-action-pins
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+
+ "github.com/github/gh-aw/pkg/workflow"
+)
+
+func main() {
+ cache := workflow.NewActionCache(".")
+ if err := cache.Load(); err != nil {
+ fmt.Fprintf(os.Stderr, "Warning: failed to load action cache: %v\n", err)
+ // Continue with empty cache — action_pins.json will be written empty.
+ }
+
+ type actionPin struct {
+ Repo string `json:"repo"`
+ Version string `json:"version"`
+ SHA string `json:"sha"`
+ Inputs map[string]*workflow.ActionYAMLInput `json:"inputs,omitempty"`
+ }
+ type actionPinsData struct {
+ Entries map[string]actionPin `json:"entries"`
+ }
+
+ entries := make(map[string]actionPin, len(cache.Entries))
+ for key, e := range cache.Entries {
+ entries[key] = actionPin{
+ Repo: e.Repo,
+ Version: e.Version,
+ SHA: e.SHA,
+ Inputs: e.Inputs,
+ }
+ }
+
+ data := actionPinsData{Entries: entries}
+ out, err := json.MarshalIndent(data, "", " ")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error marshaling action_pins.json: %v\n", err)
+ os.Exit(1)
+ }
+ out = append(out, '\n')
+
+ dest := "pkg/workflow/data/action_pins.json"
+ if err := os.WriteFile(dest, out, 0644); err != nil {
+ fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", dest, err)
+ os.Exit(1)
+ }
+
+ fmt.Printf("✓ Synced %d action pins to %s\n", len(entries), dest)
+}
From 1f75d96fe137a504ef3a2c5e529f1446624ea5b5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 14:56:20 +0000
Subject: [PATCH 2/4] fix: add comment explaining ActionDescription omission in
sync script
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/14c1b3fc-d16d-42e6-9307-d403b1bf6880
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
scripts/sync-action-pins/main.go | 3 +++
1 file changed, 3 insertions(+)
diff --git a/scripts/sync-action-pins/main.go b/scripts/sync-action-pins/main.go
index d1109552c2a..919afe76fd8 100644
--- a/scripts/sync-action-pins/main.go
+++ b/scripts/sync-action-pins/main.go
@@ -28,6 +28,9 @@ func main() {
Version string `json:"version"`
SHA string `json:"sha"`
Inputs map[string]*workflow.ActionYAMLInput `json:"inputs,omitempty"`
+ // ActionDescription is intentionally omitted: action_pins.json is the embedded
+ // fallback used only for SHA pinning; descriptions are only needed at compile time
+ // from the user-facing aw-lock.yml cache and are not part of ActionPin.
}
type actionPinsData struct {
Entries map[string]actionPin `json:"entries"`
From 0ab2b6cffc5a4750b111dfb00023365ec990b6c4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 15:26:52 +0000
Subject: [PATCH 3/4] =?UTF-8?q?fix:=20keep=20JSON=20format,=20rename=20aw-?=
=?UTF-8?q?lock.yml=20=E2=86=92=20aw-lock.json?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Per feedback, keep the lock file in JSON format (not YAML).
- CacheFileName = "aw-lock.json" (was "aw-lock.yml")
- Save/Load use JSON marshaling, new schema has version + actions + containers fields
- Convert .github/workflows/aw-lock.yml → aw-lock.json
- Convert pkg/workflow/.github/workflows/aw-lock.yml → aw-lock.json
- Update all docs, comments, and tests accordingly
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/648f89f3-a5e5-4dc2-96c1-aea60ffa8be3
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
.github/workflows/aw-lock.json | 188 ++++++++++++++++++
.github/workflows/aw-lock.yml | 148 --------------
Makefile | 6 +-
.../docs/introduction/architecture.mdx | 2 +-
.../docs/reference/compilation-process.md | 12 +-
docs/src/content/docs/reference/faq.md | 8 +-
docs/src/content/docs/reference/releases.md | 6 +-
docs/src/content/docs/setup/cli.md | 2 +-
pkg/cli/codemod_actions_lock_migration.go | 10 +-
pkg/cli/fix_codemods.go | 2 +-
pkg/cli/fix_command.go | 4 +-
pkg/cli/update_actions.go | 10 +-
pkg/cli/update_command.go | 6 +-
pkg/cli/update_command_test.go | 12 +-
pkg/cli/upgrade_command.go | 2 +-
pkg/workflow/.github/workflows/aw-lock.json | 35 ++++
pkg/workflow/.github/workflows/aw-lock.yml | 27 ---
pkg/workflow/action_cache.go | 127 ++++++------
pkg/workflow/action_cache_test.go | 8 +-
scratchpad/debugging-action-pinning.md | 36 ++--
scratchpad/layout.md | 4 +-
scripts/sync-action-pins/main.go | 4 +-
22 files changed, 347 insertions(+), 312 deletions(-)
create mode 100644 .github/workflows/aw-lock.json
delete mode 100644 .github/workflows/aw-lock.yml
create mode 100644 pkg/workflow/.github/workflows/aw-lock.json
delete mode 100644 pkg/workflow/.github/workflows/aw-lock.yml
diff --git a/.github/workflows/aw-lock.json b/.github/workflows/aw-lock.json
new file mode 100644
index 00000000000..7046be2e035
--- /dev/null
+++ b/.github/workflows/aw-lock.json
@@ -0,0 +1,188 @@
+{
+ "version": "1",
+ "actions": {
+ "actions-ecosystem/action-add-labels@v1.1.3": {
+ "repo": "actions-ecosystem/action-add-labels",
+ "version": "v1.1.3",
+ "sha": "c96b68fec76a0987cd93957189e9abd0b9a72ff1",
+ "inputs": {
+ "github_token": {
+ "default": "${{ github.token }}",
+ "description": "A GitHub token."
+ },
+ "labels": {
+ "description": "The labels' name to be added. Must be separated with line breaks if there're multiple labels.",
+ "required": true
+ },
+ "number": {
+ "description": "The number of the issue or pull request."
+ },
+ "repo": {
+ "default": "${{ github.repository }}",
+ "description": "The owner and repository name. e.g.) Codertocat/Hello-World"
+ }
+ },
+ "action_description": "Add labels to an issue or a pull request."
+ },
+ "actions/ai-inference@v2.0.8": {
+ "repo": "actions/ai-inference",
+ "version": "v2.0.8",
+ "sha": "af6ad2c4ac4edf01884054fc3a6caa6d2567c13a"
+ },
+ "actions/attest-build-provenance@v4.1.0": {
+ "repo": "actions/attest-build-provenance",
+ "version": "v4.1.0",
+ "sha": "a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32"
+ },
+ "actions/cache/restore@v5.0.4": {
+ "repo": "actions/cache/restore",
+ "version": "v5.0.4",
+ "sha": "668228422ae6a00e4ad889ee87cd7109ec5666a7"
+ },
+ "actions/cache/save@v5.0.4": {
+ "repo": "actions/cache/save",
+ "version": "v5.0.4",
+ "sha": "668228422ae6a00e4ad889ee87cd7109ec5666a7"
+ },
+ "actions/cache@v5.0.4": {
+ "repo": "actions/cache",
+ "version": "v5.0.4",
+ "sha": "668228422ae6a00e4ad889ee87cd7109ec5666a7"
+ },
+ "actions/checkout@v6.0.2": {
+ "repo": "actions/checkout",
+ "version": "v6.0.2",
+ "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd"
+ },
+ "actions/create-github-app-token@v3": {
+ "repo": "actions/create-github-app-token",
+ "version": "v3",
+ "sha": "f8d387b68d61c58ab83c6c016672934102569859"
+ },
+ "actions/download-artifact@v8.0.1": {
+ "repo": "actions/download-artifact",
+ "version": "v8.0.1",
+ "sha": "3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c"
+ },
+ "actions/github-script@v8": {
+ "repo": "actions/github-script",
+ "version": "v8",
+ "sha": "ed597411d8f924073f98dfc5c65a23a2325f34cd"
+ },
+ "actions/setup-dotnet@v5.2.0": {
+ "repo": "actions/setup-dotnet",
+ "version": "v5.2.0",
+ "sha": "c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7"
+ },
+ "actions/setup-go@v6.4.0": {
+ "repo": "actions/setup-go",
+ "version": "v6.4.0",
+ "sha": "4a3601121dd01d1626a1e23e37211e3254c1c06c"
+ },
+ "actions/setup-java@v5.2.0": {
+ "repo": "actions/setup-java",
+ "version": "v5.2.0",
+ "sha": "be666c2fcd27ec809703dec50e508c2fdc7f6654"
+ },
+ "actions/setup-node@v6.3.0": {
+ "repo": "actions/setup-node",
+ "version": "v6.3.0",
+ "sha": "53b83947a5a98c8d113130e565377fae1a50d02f"
+ },
+ "actions/setup-python@v6.2.0": {
+ "repo": "actions/setup-python",
+ "version": "v6.2.0",
+ "sha": "a309ff8b426b58ec0e2a45f0f869d46889d02405"
+ },
+ "actions/upload-artifact@v7": {
+ "repo": "actions/upload-artifact",
+ "version": "v7",
+ "sha": "bbbca2ddaa5d8feaa63e36b76fdaad77386f024f"
+ },
+ "anchore/sbom-action@v0.24.0": {
+ "repo": "anchore/sbom-action",
+ "version": "v0.24.0",
+ "sha": "e22c389904149dbc22b58101806040fa8d37a610"
+ },
+ "astral-sh/setup-uv@v8.0.0": {
+ "repo": "astral-sh/setup-uv",
+ "version": "v8.0.0",
+ "sha": "cec208311dfd045dd5311c1add060b2062131d57"
+ },
+ "cli/gh-extension-precompile@v2.1.0": {
+ "repo": "cli/gh-extension-precompile",
+ "version": "v2.1.0",
+ "sha": "9e2237c30f869ad3bcaed6a4be2cd43564dd421b"
+ },
+ "denoland/setup-deno@v2.0.4": {
+ "repo": "denoland/setup-deno",
+ "version": "v2.0.4",
+ "sha": "667a34cdef165d8d2b2e98dde39547c9daac7282"
+ },
+ "docker/build-push-action@v7": {
+ "repo": "docker/build-push-action",
+ "version": "v7",
+ "sha": "d08e5c354a6adb9ed34480a06d141179aa583294"
+ },
+ "docker/login-action@v4.1.0": {
+ "repo": "docker/login-action",
+ "version": "v4.1.0",
+ "sha": "4907a6ddec9925e35a0a9e82d7399ccc52663121"
+ },
+ "docker/metadata-action@v6": {
+ "repo": "docker/metadata-action",
+ "version": "v6",
+ "sha": "030e881283bb7a6894de51c315a6bfe6a94e05cf"
+ },
+ "docker/setup-buildx-action@v4": {
+ "repo": "docker/setup-buildx-action",
+ "version": "v4",
+ "sha": "4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd"
+ },
+ "erlef/setup-beam@v1.24": {
+ "repo": "erlef/setup-beam",
+ "version": "v1.24",
+ "sha": "3077b49d03fe1e281e5e5bc79084f079e9bf93ca"
+ },
+ "github/codeql-action/upload-sarif@v4.35.1": {
+ "repo": "github/codeql-action/upload-sarif",
+ "version": "v4.35.1",
+ "sha": "0e9f55954318745b37b7933c693bc093f7336125"
+ },
+ "github/gh-aw-actions/setup@v0.67.2": {
+ "repo": "github/gh-aw-actions/setup",
+ "version": "v0.67.2",
+ "sha": "03e31e064a68e8d5ad890c92f303cfb5a3536006"
+ },
+ "github/stale-repos@v9.0.7": {
+ "repo": "github/stale-repos",
+ "version": "v9.0.7",
+ "sha": "25946246f29e8692a397502045e457c4dc96c6e4"
+ },
+ "haskell-actions/setup@v2.10.3": {
+ "repo": "haskell-actions/setup",
+ "version": "v2.10.3",
+ "sha": "9cd1b7bf3f36d5a3c3b17abc3545bfb5481912ea"
+ },
+ "microsoft/apm-action@v1.4.1": {
+ "repo": "microsoft/apm-action",
+ "version": "v1.4.1",
+ "sha": "a190b0b1a91031057144dc136acf9757a59c9e4d"
+ },
+ "oven-sh/setup-bun@v2.2.0": {
+ "repo": "oven-sh/setup-bun",
+ "version": "v2.2.0",
+ "sha": "0c5077e51419868618aeaa5fe8019c62421857d6"
+ },
+ "ruby/setup-ruby@v1.300.0": {
+ "repo": "ruby/setup-ruby",
+ "version": "v1.300.0",
+ "sha": "e65c17d16e57e481586a6a5a0282698790062f92"
+ },
+ "super-linter/super-linter@v8.6.0": {
+ "repo": "super-linter/super-linter",
+ "version": "v8.6.0",
+ "sha": "9e863354e3ff62e0727d37183162c4a88873df41"
+ }
+ }
+}
diff --git a/.github/workflows/aw-lock.yml b/.github/workflows/aw-lock.yml
deleted file mode 100644
index 36c6fdb9668..00000000000
--- a/.github/workflows/aw-lock.yml
+++ /dev/null
@@ -1,148 +0,0 @@
-version: "1"
-actions:
- 'actions-ecosystem/action-add-labels@v1.1.3':
- repo: actions-ecosystem/action-add-labels
- version: v1.1.3
- sha: c96b68fec76a0987cd93957189e9abd0b9a72ff1
- inputs:
- github_token:
- description: A GitHub token.
- default: ${{ github.token }}
- labels:
- description: The labels' name to be added. Must be separated with line breaks if there're multiple labels.
- required: true
- number:
- description: The number of the issue or pull request.
- repo:
- description: The owner and repository name. e.g.) Codertocat/Hello-World
- default: ${{ github.repository }}
- action_description: Add labels to an issue or a pull request.
- 'actions/ai-inference@v2.0.8':
- repo: actions/ai-inference
- version: v2.0.8
- sha: af6ad2c4ac4edf01884054fc3a6caa6d2567c13a
- 'actions/attest-build-provenance@v4.1.0':
- repo: actions/attest-build-provenance
- version: v4.1.0
- sha: a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32
- 'actions/cache/restore@v5.0.4':
- repo: actions/cache/restore
- version: v5.0.4
- sha: 668228422ae6a00e4ad889ee87cd7109ec5666a7
- 'actions/cache/save@v5.0.4':
- repo: actions/cache/save
- version: v5.0.4
- sha: 668228422ae6a00e4ad889ee87cd7109ec5666a7
- 'actions/cache@v5.0.4':
- repo: actions/cache
- version: v5.0.4
- sha: 668228422ae6a00e4ad889ee87cd7109ec5666a7
- 'actions/checkout@v6.0.2':
- repo: actions/checkout
- version: v6.0.2
- sha: de0fac2e4500dabe0009e67214ff5f5447ce83dd
- 'actions/create-github-app-token@v3':
- repo: actions/create-github-app-token
- version: v3
- sha: f8d387b68d61c58ab83c6c016672934102569859
- 'actions/download-artifact@v8.0.1':
- repo: actions/download-artifact
- version: v8.0.1
- sha: 3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c
- 'actions/github-script@v8':
- repo: actions/github-script
- version: v8
- sha: ed597411d8f924073f98dfc5c65a23a2325f34cd
- 'actions/setup-dotnet@v5.2.0':
- repo: actions/setup-dotnet
- version: v5.2.0
- sha: c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7
- 'actions/setup-go@v6.4.0':
- repo: actions/setup-go
- version: v6.4.0
- sha: 4a3601121dd01d1626a1e23e37211e3254c1c06c
- 'actions/setup-java@v5.2.0':
- repo: actions/setup-java
- version: v5.2.0
- sha: be666c2fcd27ec809703dec50e508c2fdc7f6654
- 'actions/setup-node@v6.3.0':
- repo: actions/setup-node
- version: v6.3.0
- sha: 53b83947a5a98c8d113130e565377fae1a50d02f
- 'actions/setup-python@v6.2.0':
- repo: actions/setup-python
- version: v6.2.0
- sha: a309ff8b426b58ec0e2a45f0f869d46889d02405
- 'actions/upload-artifact@v7':
- repo: actions/upload-artifact
- version: v7
- sha: bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
- 'anchore/sbom-action@v0.24.0':
- repo: anchore/sbom-action
- version: v0.24.0
- sha: e22c389904149dbc22b58101806040fa8d37a610
- 'astral-sh/setup-uv@v8.0.0':
- repo: astral-sh/setup-uv
- version: v8.0.0
- sha: cec208311dfd045dd5311c1add060b2062131d57
- 'cli/gh-extension-precompile@v2.1.0':
- repo: cli/gh-extension-precompile
- version: v2.1.0
- sha: 9e2237c30f869ad3bcaed6a4be2cd43564dd421b
- 'denoland/setup-deno@v2.0.4':
- repo: denoland/setup-deno
- version: v2.0.4
- sha: 667a34cdef165d8d2b2e98dde39547c9daac7282
- 'docker/build-push-action@v7':
- repo: docker/build-push-action
- version: v7
- sha: d08e5c354a6adb9ed34480a06d141179aa583294
- 'docker/login-action@v4.1.0':
- repo: docker/login-action
- version: v4.1.0
- sha: 4907a6ddec9925e35a0a9e82d7399ccc52663121
- 'docker/metadata-action@v6':
- repo: docker/metadata-action
- version: v6
- sha: 030e881283bb7a6894de51c315a6bfe6a94e05cf
- 'docker/setup-buildx-action@v4':
- repo: docker/setup-buildx-action
- version: v4
- sha: 4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd
- 'erlef/setup-beam@v1.24':
- repo: erlef/setup-beam
- version: v1.24
- sha: 3077b49d03fe1e281e5e5bc79084f079e9bf93ca
- 'github/codeql-action/upload-sarif@v4.35.1':
- repo: github/codeql-action/upload-sarif
- version: v4.35.1
- sha: 0e9f55954318745b37b7933c693bc093f7336125
- 'github/gh-aw-actions/setup@v0.67.2':
- repo: github/gh-aw-actions/setup
- version: v0.67.2
- sha: 03e31e064a68e8d5ad890c92f303cfb5a3536006
- 'github/stale-repos@v9.0.7':
- repo: github/stale-repos
- version: v9.0.7
- sha: 25946246f29e8692a397502045e457c4dc96c6e4
- 'haskell-actions/setup@v2.10.3':
- repo: haskell-actions/setup
- version: v2.10.3
- sha: 9cd1b7bf3f36d5a3c3b17abc3545bfb5481912ea
- 'microsoft/apm-action@v1.4.1':
- repo: microsoft/apm-action
- version: v1.4.1
- sha: a190b0b1a91031057144dc136acf9757a59c9e4d
- 'oven-sh/setup-bun@v2.2.0':
- repo: oven-sh/setup-bun
- version: v2.2.0
- sha: 0c5077e51419868618aeaa5fe8019c62421857d6
- 'ruby/setup-ruby@v1.300.0':
- repo: ruby/setup-ruby
- version: v1.300.0
- sha: e65c17d16e57e481586a6a5a0282698790062f92
- 'super-linter/super-linter@v8.6.0':
- repo: super-linter/super-linter
- version: v8.6.0
- sha: 9e863354e3ff62e0727d37183162c4a88873df41
-
diff --git a/Makefile b/Makefile
index 4bf9d6c0ef5..6242ddbd65d 100644
--- a/Makefile
+++ b/Makefile
@@ -665,10 +665,10 @@ clean-docs:
@echo "✓ Documentation artifacts cleaned"
# Sync templates from .github to pkg/cli/templates
-# Sync action pins from .github/workflows/aw-lock.yml to pkg/workflow/data
+# Sync action pins from .github/workflows/aw-lock.json to pkg/workflow/data
.PHONY: sync-action-pins
sync-action-pins:
- @echo "Syncing action pins from .github/workflows/aw-lock.yml to pkg/workflow/data/action_pins.json..."
+ @echo "Syncing action pins from .github/workflows/aw-lock.json to pkg/workflow/data/action_pins.json..."
@go run ./scripts/sync-action-pins
@echo "✓ Action pins synced successfully"
@@ -802,7 +802,7 @@ help:
@echo " actionlint - Validate workflows with actionlint (depends on build)"
@echo " validate-workflows - Validate compiled workflow lock files (depends on build)"
@echo " install - Install binary locally"
- @echo " sync-action-pins - Sync action pins from .github/workflows/aw-lock.yml to pkg/workflow/data (runs automatically during build)"
+ @echo " sync-action-pins - Sync action pins from .github/workflows/aw-lock.json to pkg/workflow/data (runs automatically during build)"
@echo " sync-action-scripts - Sync install-gh-aw.sh to actions/setup-cli/install.sh (runs automatically during build)"
@echo " update - Update GitHub Actions and workflows, sync action pins, and rebuild binary"
@echo " fix - Apply automatic codemod-style fixes to workflow files (depends on build)"
diff --git a/docs/src/content/docs/introduction/architecture.mdx b/docs/src/content/docs/introduction/architecture.mdx
index 81fc5033bdb..1c547536ab9 100644
--- a/docs/src/content/docs/introduction/architecture.mdx
+++ b/docs/src/content/docs/introduction/architecture.mdx
@@ -416,7 +416,7 @@ flowchart TB
subgraph Pinning["Action Pinning"]
SHA["SHA Resolution
actions/checkout@sha # v4"]
- CACHE[/"aw-lock.yml
(Cached SHAs)"/]
+ CACHE[/"aw-lock.json
(Cached SHAs)"/]
end
subgraph Scanners["Security Scanners"]
diff --git a/docs/src/content/docs/reference/compilation-process.md b/docs/src/content/docs/reference/compilation-process.md
index 25a6e045d77..f115ca7f2f9 100644
--- a/docs/src/content/docs/reference/compilation-process.md
+++ b/docs/src/content/docs/reference/compilation-process.md
@@ -203,15 +203,15 @@ Data flows via GitHub Actions artifacts: agent writes `agent_output.json` → de
## Action Pinning
-All GitHub Actions are pinned to commit SHAs (e.g., `actions/checkout@b4ffde6...11 # v6`) to prevent supply chain attacks. Tags can be moved to malicious commits, but SHA commits are immutable. The resolution order mirrors Phase 4: cache (`.github/workflows/aw-lock.yml`) → GitHub API → embedded pins.
+All GitHub Actions are pinned to commit SHAs (e.g., `actions/checkout@b4ffde6...11 # v6`) to prevent supply chain attacks. Tags can be moved to malicious commits, but SHA commits are immutable. The resolution order mirrors Phase 4: cache (`.github/workflows/aw-lock.json`) → GitHub API → embedded pins.
-### The aw-lock.yml Cache
+### The aw-lock.json Cache
-`.github/workflows/aw-lock.yml` stores resolved `action@version` → SHA mappings so that compilation produces consistent results regardless of the token available. Resolving a version tag to a SHA requires querying the GitHub API, which can fail when the token has limited permissions — notably when compiling via GitHub Copilot Coding Agent (CCA), which uses a restricted token that may not have access to external repositories.
+`.github/workflows/aw-lock.json` stores resolved `action@version` → SHA mappings so that compilation produces consistent results regardless of the token available. Resolving a version tag to a SHA requires querying the GitHub API, which can fail when the token has limited permissions — notably when compiling via GitHub Copilot Coding Agent (CCA), which uses a restricted token that may not have access to external repositories.
By caching SHA resolutions from a prior compilation (done with a user PAT or a GitHub Actions token with broader scope), subsequent compilations reuse those SHAs without making API calls. Without the cache, compilation is unstable: it succeeds with a permissive token but fails when token access is restricted.
-**Commit `aw-lock.yml` to version control.** This ensures all contributors and automated tools, including CCA, use the same immutable pins. Refresh it periodically with `gh aw update-actions`, or delete it and recompile with an appropriate token to force full re-resolution.
+**Commit `aw-lock.json` to version control.** This ensures all contributors and automated tools, including CCA, use the same immutable pins. Refresh it periodically with `gh aw update-actions`, or delete it and recompile with an appropriate token to force full re-resolution.
## The gh-aw-actions Repository
@@ -316,7 +316,7 @@ Pre-activation runs checks sequentially. Any failure sets `activated=false`, pre
## Performance Optimization
-**Compilation speed**: Simple workflows compile in ~100ms, complex workflows with imports in ~500ms, and workflows with dynamic action resolution in ~2s. Optimize by using action cache (`.github/workflows/aw-lock.yml`), minimizing import depth, and pre-compiling shared workflows.
+**Compilation speed**: Simple workflows compile in ~100ms, complex workflows with imports in ~500ms, and workflows with dynamic action resolution in ~2s. Optimize by using action cache (`.github/workflows/aw-lock.json`), minimizing import depth, and pre-compiling shared workflows.
**Runtime performance**: Safe output jobs without dependencies run in parallel. Enable `cache:` for dependencies, use `cache-memory:` for persistent agent memory, and cache action resolutions for faster compilation.
@@ -332,7 +332,7 @@ Pre-activation runs checks sequentially. Any failure sets `activated=false`, pre
**Security**: Always use action pinning (never floating tags), enable threat detection (`safe-outputs.threat-detection:`), limit tool access with `allowed:`, review generated `.lock.yml` files, and run security scanners (`--actionlint --zizmor --poutine`).
-**Maintainability**: Use imports for shared configuration, document complex workflows with `description:`, compile frequently during development, version control lock files and action pins (`.github/workflows/aw-lock.yml`).
+**Maintainability**: Use imports for shared configuration, document complex workflows with `description:`, compile frequently during development, version control lock files and action pins (`.github/workflows/aw-lock.json`).
**Performance**: Enable caching (`cache:` and `cache-memory:`), minimize imports to essentials, optimize tool configurations with restricted `allowed:` lists, use safe-jobs for custom logic.
diff --git a/docs/src/content/docs/reference/faq.md b/docs/src/content/docs/reference/faq.md
index 67963f13767..08972c28f5d 100644
--- a/docs/src/content/docs/reference/faq.md
+++ b/docs/src/content/docs/reference/faq.md
@@ -270,13 +270,13 @@ Both files should be committed to version control:
- **`.md` file**: Your source - edit the prompt body freely; changes take effect at the next run without recompiling
- **`.lock.yml` file**: The compiled workflow GitHub Actions actually runs; must be regenerated after any frontmatter changes (permissions, tools, triggers)
-### What is the aw-lock.yml file?
+### What is the aw-lock.json file?
-The `.github/workflows/aw-lock.yml` file is a cache of resolved `action@version` → ref mappings. During compilation, the compiler **tries** to pin each action reference to an immutable commit SHA for security. Resolving a version tag to a SHA requires querying the GitHub API (scanning releases), which can fail when the available token has limited permissions — for example, when compiling via GitHub Copilot Coding Agent (CCA) where the token may not have access to external repositories. In those cases, the compiler may fall back to leaving a stable version tag ref (such as `@v0`) instead of a SHA.
+The `.github/workflows/aw-lock.json` file is a cache of resolved `action@version` → ref mappings. During compilation, the compiler **tries** to pin each action reference to an immutable commit SHA for security. Resolving a version tag to a SHA requires querying the GitHub API (scanning releases), which can fail when the available token has limited permissions — for example, when compiling via GitHub Copilot Coding Agent (CCA) where the token may not have access to external repositories. In those cases, the compiler may fall back to leaving a stable version tag ref (such as `@v0`) instead of a SHA.
-The cache avoids this problem: if a ref (typically a SHA) was previously resolved (using a user PAT or a GitHub Actions token with broader access), the result is stored in `aw-lock.yml` and reused on subsequent compilations, regardless of the current token's capabilities. Without this cache, compilation is unstable — it succeeds with a permissive token but fails when token access is restricted.
+The cache avoids this problem: if a ref (typically a SHA) was previously resolved (using a user PAT or a GitHub Actions token with broader access), the result is stored in `aw-lock.json` and reused on subsequent compilations, regardless of the current token's capabilities. Without this cache, compilation is unstable — it succeeds with a permissive token but fails when token access is restricted.
-Commit `aw-lock.yml` to version control so that all contributors and automated tools (including CCA) use consistent action refs (SHAs or version tags) without needing to re-resolve them. Refresh the cache periodically with `gh aw update-actions`, or delete it and recompile to force a full re-resolution when you have an appropriate token. See [Action Pinning](/gh-aw/reference/compilation-process/#action-pinning) for details.
+Commit `aw-lock.json` to version control so that all contributors and automated tools (including CCA) use consistent action refs (SHAs or version tags) without needing to re-resolve them. Refresh the cache periodically with `gh aw update-actions`, or delete it and recompile to force a full re-resolution when you have an appropriate token. See [Action Pinning](/gh-aw/reference/compilation-process/#action-pinning) for details.
### What is `github/gh-aw-actions`?
diff --git a/docs/src/content/docs/reference/releases.md b/docs/src/content/docs/reference/releases.md
index 0f66ffbb78d..1d1bad0a20e 100644
--- a/docs/src/content/docs/reference/releases.md
+++ b/docs/src/content/docs/reference/releases.md
@@ -81,12 +81,12 @@ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
SHA pins are immutable — unlike tags, they cannot be silently redirected to a different commit. This protects workflows from supply-chain attacks.
-The resolved SHA mappings are cached in `.github/workflows/aw-lock.yml`. Commit this file to version control so that all contributors and automated tools (including GitHub Copilot Coding Agent) produce identical lock files without needing broad API access.
+The resolved SHA mappings are cached in `.github/workflows/aw-lock.json`. Commit this file to version control so that all contributors and automated tools (including GitHub Copilot Coding Agent) produce identical lock files without needing broad API access.
To refresh action pins:
```bash
-gh aw update-actions # Update aw-lock.yml to latest SHAs
+gh aw update-actions # Update aw-lock.json to latest SHAs
gh aw compile # Recompile workflows using the refreshed pins
```
@@ -104,7 +104,7 @@ These two commands address different concerns:
1. Self-updates the `gh aw` extension to the latest version
2. Regenerates the dispatcher agent file (like `gh aw init`)
3. Applies codemods to fix deprecated syntax across all workflow markdown files
-4. Updates GitHub Actions versions in `aw-lock.yml`
+4. Updates GitHub Actions versions in `aw-lock.json`
5. Recompiles all workflows to produce fresh `.lock.yml` files
Run `upgrade` after installing a new version of `gh aw`, or periodically to keep your repository current.
diff --git a/docs/src/content/docs/setup/cli.md b/docs/src/content/docs/setup/cli.md
index 8d89fe0becf..e5a593bf48c 100644
--- a/docs/src/content/docs/setup/cli.md
+++ b/docs/src/content/docs/setup/cli.md
@@ -534,7 +534,7 @@ gh aw remove my-workflow --keep-orphans # Remove but keep orphaned include file
Update workflows based on `source` field (`owner/repo/path@ref`). By default, performs a 3-way merge to preserve local changes; use `--no-merge` to override with upstream. Semantic versions update within same major version.
-By default, `update` also force-updates all GitHub Actions referenced in your workflows (both in `aw-lock.yml` and workflow files) to their latest major version. Use `--disable-release-bump` to restrict force-updates to core `actions/*` actions only.
+By default, `update` also force-updates all GitHub Actions referenced in your workflows (both in `aw-lock.json` and workflow files) to their latest major version. Use `--disable-release-bump` to restrict force-updates to core `actions/*` actions only.
If no workflows in the repository contain a `source` field, the command exits gracefully with an informational message rather than an error. This is expected behavior for repositories that have not yet added updatable workflows.
diff --git a/pkg/cli/codemod_actions_lock_migration.go b/pkg/cli/codemod_actions_lock_migration.go
index 679c6c62beb..aad6b6cd37b 100644
--- a/pkg/cli/codemod_actions_lock_migration.go
+++ b/pkg/cli/codemod_actions_lock_migration.go
@@ -10,14 +10,14 @@ import (
)
// getActionsLockMigrationCodemod returns a file-level codemod that migrates the
-// old .github/aw/actions-lock.json to the new .github/workflows/aw-lock.yml format.
+// old .github/aw/actions-lock.json to the new .github/workflows/aw-lock.json format.
// The Apply function is a no-op (it doesn't modify workflow files); the actual
// migration is performed by MigrateActionsLockFile which is called from fix_command.go.
func getActionsLockMigrationCodemod() Codemod {
return Codemod{
ID: "migrate-actions-lock-file",
- Name: "Migrate actions-lock.json to aw-lock.yml",
- Description: "Moves .github/aw/actions-lock.json to .github/workflows/aw-lock.yml with the new YAML format",
+ Name: "Migrate actions-lock.json to aw-lock.json",
+ Description: "Moves .github/aw/actions-lock.json to .github/workflows/aw-lock.json with the new JSON format",
IntroducedIn: "0.71.0",
Apply: func(content string, frontmatter map[string]any) (string, bool, error) {
// This codemod is handled by MigrateActionsLockFile (called from fix_command.go).
@@ -28,7 +28,7 @@ func getActionsLockMigrationCodemod() Codemod {
}
// MigrateActionsLockFile moves .github/aw/actions-lock.json to
-// .github/workflows/aw-lock.yml and converts the format from JSON to YAML.
+// .github/workflows/aw-lock.json and migrates the format (entries → actions, adds version).
// Returns (migrated, error): migrated is true when the migration was performed.
func MigrateActionsLockFile(write bool, verbose bool) (bool, error) {
legacyPath := filepath.Join(".github", "aw", workflow.LegacyCacheFileName)
@@ -62,7 +62,7 @@ func MigrateActionsLockFile(write bool, verbose bool) (bool, error) {
}
// Load via ActionCache (which handles the legacy JSON format) and re-save
- // to the new YAML path.
+ // to the new path with the updated schema (entries → actions, adds version).
cache := workflow.NewActionCache(".")
if err := cache.Load(); err != nil {
return false, fmt.Errorf("loading %s: %w", legacyPath, err)
diff --git a/pkg/cli/fix_codemods.go b/pkg/cli/fix_codemods.go
index 6a7b2b9f24f..4ccaccd2a40 100644
--- a/pkg/cli/fix_codemods.go
+++ b/pkg/cli/fix_codemods.go
@@ -51,7 +51,7 @@ func GetAllCodemods() []Codemod {
getPluginsToDependenciesCodemod(), // Migrate plugins to dependencies (plugins removed in favour of APM)
getGitHubReposToAllowedReposCodemod(), // Rename deprecated tools.github.repos to tools.github.allowed-repos
getDIFCProxyToIntegrityProxyCodemod(), // Migrate deprecated features.difc-proxy to tools.github.integrity-proxy
- getActionsLockMigrationCodemod(), // Migrate .github/aw/actions-lock.json to .github/workflows/aw-lock.yml
+ getActionsLockMigrationCodemod(), // Migrate .github/aw/actions-lock.json to .github/workflows/aw-lock.json
}
fixCodemodsLog.Printf("Loaded codemod registry: %d codemods available", len(codemods))
return codemods
diff --git a/pkg/cli/fix_command.go b/pkg/cli/fix_command.go
index 9f358ff92f6..c780e3a813f 100644
--- a/pkg/cli/fix_command.go
+++ b/pkg/cli/fix_command.go
@@ -50,7 +50,7 @@ The command will:
Without --write (dry-run mode), no files are modified. With --write, the command performs
all steps and additionally:
4. Write updated files back to disk
- 5. Migrate .github/aw/actions-lock.json to .github/workflows/aw-lock.yml if it exists
+ 5. Migrate .github/aw/actions-lock.json to .github/workflows/aw-lock.json if it exists
6. Delete deprecated .github/aw/schemas/agentic-workflow.json file if it exists
7. Delete old template files from previous versions if present
8. Delete old workflow-specific .agent.md files from .github/agents/ if present
@@ -206,7 +206,7 @@ func runFixCommand(workflowIDs []string, write bool, verbose bool, workflowDir s
}
}
- // Migrate legacy .github/aw/actions-lock.json → .github/workflows/aw-lock.yml
+ // Migrate legacy .github/aw/actions-lock.json → .github/workflows/aw-lock.json
fixLog.Print("Checking for legacy actions-lock.json to migrate")
if _, err := MigrateActionsLockFile(write, verbose); err != nil {
fixLog.Printf("Failed to migrate actions-lock.json: %v", err)
diff --git a/pkg/cli/update_actions.go b/pkg/cli/update_actions.go
index 763a0e972eb..2696424de0f 100644
--- a/pkg/cli/update_actions.go
+++ b/pkg/cli/update_actions.go
@@ -22,7 +22,7 @@ func isCoreAction(repo string) bool {
return strings.HasPrefix(repo, "actions/")
}
-// UpdateActions updates GitHub Actions versions in .github/workflows/aw-lock.yml
+// UpdateActions updates GitHub Actions versions in .github/workflows/aw-lock.json
// It checks each action for newer releases and updates the SHA if a newer version is found.
// By default all actions are updated to the latest major version; pass disableReleaseBump=true
// to revert to the old behaviour where only core (actions/*) actions bypass the --major flag.
@@ -37,7 +37,7 @@ func UpdateActions(allowMajor, verbose, disableReleaseBump bool) error {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Checking for GitHub Actions updates..."))
}
- // Load the action cache (aw-lock.yml) using the shared ActionCache helpers
+ // Load the action cache (aw-lock.json) using the shared ActionCache helpers
// so that cached inputs/descriptions for safe-outputs.actions entries are preserved.
// NewActionCache also handles loading from the legacy actions-lock.json path.
awLockPath := filepath.Join(".github", "workflows", workflow.CacheFileName)
@@ -56,7 +56,7 @@ func UpdateActions(allowMajor, verbose, disableReleaseBump bool) error {
return fmt.Errorf("failed to parse actions lock file: %w", err)
}
- updateLog.Printf("Loaded %d action entries from aw-lock.yml", len(actionCache.Entries))
+ updateLog.Printf("Loaded %d action entries from aw-lock.json", len(actionCache.Entries))
// Track updates
var updatedActions []string
@@ -157,8 +157,8 @@ func UpdateActions(allowMajor, verbose, disableReleaseBump bool) error {
return fmt.Errorf("failed to save actions lock file: %w", err)
}
- updateLog.Printf("Successfully wrote updated aw-lock.yml with %d updates", len(updatedActions))
- fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Updated aw-lock.yml file"))
+ updateLog.Printf("Successfully wrote updated aw-lock.json with %d updates", len(updatedActions))
+ fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Updated aw-lock.json file"))
}
return nil
diff --git a/pkg/cli/update_command.go b/pkg/cli/update_command.go
index b938dcf2904..e9c53608ec4 100644
--- a/pkg/cli/update_command.go
+++ b/pkg/cli/update_command.go
@@ -118,13 +118,13 @@ func RunUpdateWorkflows(workflowNames []string, allowMajor, force, verbose bool,
firstErr = fmt.Errorf("workflow update failed: %w", err)
}
- // Update GitHub Actions versions in aw-lock.yml.
+ // Update GitHub Actions versions in aw-lock.json.
// By default all actions are updated to the latest major version.
// Pass --disable-release-bump to revert to only forcing updates for core (actions/*) actions.
- updateLog.Printf("Updating GitHub Actions versions in aw-lock.yml: allowMajor=%v, disableReleaseBump=%v", allowMajor, disableReleaseBump)
+ updateLog.Printf("Updating GitHub Actions versions in aw-lock.json: allowMajor=%v, disableReleaseBump=%v", allowMajor, disableReleaseBump)
if err := UpdateActions(allowMajor, verbose, disableReleaseBump); err != nil {
// Non-fatal: warn but don't fail the update
- fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to update aw-lock.yml: %v", err)))
+ fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Warning: Failed to update aw-lock.json: %v", err)))
}
// Update action references in user-provided steps within workflow .md files.
diff --git a/pkg/cli/update_command_test.go b/pkg/cli/update_command_test.go
index c0b08e0260e..dbc4791cebf 100644
--- a/pkg/cli/update_command_test.go
+++ b/pkg/cli/update_command_test.go
@@ -757,20 +757,20 @@ func TestMarshalActionsLockSorted(t *testing.T) {
t.Errorf("Expected actions/checkout to appear before zebra/action in sorted output")
}
- // Check YAML structure
- if !strings.Contains(result, "actions:") {
- t.Error("Expected 'actions:' key in YAML output")
+ // Check JSON structure
+ if !strings.Contains(result, `"actions":`) {
+ t.Error("Expected 'actions' key in JSON output")
}
- if !strings.Contains(result, "repo: actions/checkout") {
+ if !strings.Contains(result, `"repo": "actions/checkout"`) {
t.Error("Expected actions/checkout repo in output")
}
- if !strings.Contains(result, "version: v5") {
+ if !strings.Contains(result, `"version": "v5"`) {
t.Error("Expected v5 version in output")
}
- if !strings.Contains(result, "sha: def456") {
+ if !strings.Contains(result, `"sha": "def456"`) {
t.Error("Expected def456 SHA in output")
}
}
diff --git a/pkg/cli/upgrade_command.go b/pkg/cli/upgrade_command.go
index a0ce2dd1728..cebcea2dfbd 100644
--- a/pkg/cli/upgrade_command.go
+++ b/pkg/cli/upgrade_command.go
@@ -38,7 +38,7 @@ func NewUpgradeCommand() *cobra.Command {
This command:
1. Updates the dispatcher agent file to the latest template (like 'init' command)
2. Applies automatic codemods to fix deprecated fields in all workflows (like 'fix --write')
- 3. Updates GitHub Actions versions in .github/workflows/aw-lock.yml (unless --no-actions is set)
+ 3. Updates GitHub Actions versions in .github/workflows/aw-lock.json (unless --no-actions is set)
4. Compiles all workflows to generate lock files (like 'compile' command)
DEPENDENCY HEALTH AUDIT:
diff --git a/pkg/workflow/.github/workflows/aw-lock.json b/pkg/workflow/.github/workflows/aw-lock.json
new file mode 100644
index 00000000000..26acd621be4
--- /dev/null
+++ b/pkg/workflow/.github/workflows/aw-lock.json
@@ -0,0 +1,35 @@
+{
+ "version": "1",
+ "actions": {
+ "actions/ai-inference@v1": {
+ "repo": "actions/ai-inference",
+ "version": "v1",
+ "sha": "b81b2afb8390ee6839b494a404766bef6493c7d9"
+ },
+ "actions/checkout@v4": {
+ "repo": "actions/checkout",
+ "version": "v4",
+ "sha": "08eba0b27e820071cde6df949e0beb9ba4906955"
+ },
+ "actions/checkout@v5": {
+ "repo": "actions/checkout",
+ "version": "v5",
+ "sha": "93cb6efe18208431cddfb8368fd83d5badbf9bfd"
+ },
+ "actions/checkout@v6": {
+ "repo": "actions/checkout",
+ "version": "v6.0.2",
+ "sha": "de0fac2e4500dabe0009e67214ff5f5447ce83dd"
+ },
+ "actions/setup-node@v4": {
+ "repo": "actions/setup-node",
+ "version": "v4",
+ "sha": "49933ea5288caeca8642d1e84afbd3f7d6820020"
+ },
+ "actions/setup-node@v6": {
+ "repo": "actions/setup-node",
+ "version": "v6",
+ "sha": "2028fbc5c25fe9cf00d9f06a71cc4710d4507903"
+ }
+ }
+}
diff --git a/pkg/workflow/.github/workflows/aw-lock.yml b/pkg/workflow/.github/workflows/aw-lock.yml
deleted file mode 100644
index f4a4b1f8f96..00000000000
--- a/pkg/workflow/.github/workflows/aw-lock.yml
+++ /dev/null
@@ -1,27 +0,0 @@
-version: "1"
-actions:
- 'actions/ai-inference@v1':
- repo: actions/ai-inference
- version: v1
- sha: b81b2afb8390ee6839b494a404766bef6493c7d9
- 'actions/checkout@v4':
- repo: actions/checkout
- version: v4
- sha: 08eba0b27e820071cde6df949e0beb9ba4906955
- 'actions/checkout@v5':
- repo: actions/checkout
- version: v5
- sha: 93cb6efe18208431cddfb8368fd83d5badbf9bfd
- 'actions/checkout@v6':
- repo: actions/checkout
- version: v6.0.2
- sha: de0fac2e4500dabe0009e67214ff5f5447ce83dd
- 'actions/setup-node@v4':
- repo: actions/setup-node
- version: v4
- sha: 49933ea5288caeca8642d1e84afbd3f7d6820020
- 'actions/setup-node@v6':
- repo: actions/setup-node
- version: v6
- sha: 2028fbc5c25fe9cf00d9f06a71cc4710d4507903
-
diff --git a/pkg/workflow/action_cache.go b/pkg/workflow/action_cache.go
index cde0af3306f..71251d6aca3 100644
--- a/pkg/workflow/action_cache.go
+++ b/pkg/workflow/action_cache.go
@@ -9,43 +9,42 @@ import (
"strings"
"github.com/github/gh-aw/pkg/logger"
- "go.yaml.in/yaml/v3"
)
var actionCacheLog = logger.New("workflow:action_cache")
const (
// CacheFileName is the name of the lock file in .github/workflows/.
- CacheFileName = "aw-lock.yml"
+ CacheFileName = "aw-lock.json"
// LegacyCacheFileName is the old name of the lock file in .github/aw/.
// Used for backward-compatible loading and migration codemods.
LegacyCacheFileName = "actions-lock.json"
- // awLockFileVersion is the current version of the aw-lock.yml format.
+ // awLockFileVersion is the current version of the aw-lock.json format.
awLockFileVersion = "1"
)
// ActionCacheEntry represents a cached action pin resolution.
type ActionCacheEntry struct {
- Repo string `json:"repo" yaml:"repo"`
- Version string `json:"version" yaml:"version"`
- SHA string `json:"sha" yaml:"sha"`
- Inputs map[string]*ActionYAMLInput `json:"inputs,omitempty" yaml:"inputs,omitempty"` // cached inputs from action.yml
- ActionDescription string `json:"action_description,omitempty" yaml:"action_description,omitempty"` // cached description from action.yml
+ Repo string `json:"repo"`
+ Version string `json:"version"`
+ SHA string `json:"sha"`
+ Inputs map[string]*ActionYAMLInput `json:"inputs,omitempty"` // cached inputs from action.yml
+ ActionDescription string `json:"action_description,omitempty"` // cached description from action.yml
}
// ContainerPinEntry represents a cached container image pin resolution.
type ContainerPinEntry struct {
- Image string `yaml:"image"`
- Digest string `yaml:"digest"`
+ Image string `json:"image"`
+ Digest string `json:"digest"`
}
-// awLockFileFormat is the on-disk representation of aw-lock.yml.
+// awLockFileFormat is the on-disk representation of aw-lock.json.
type awLockFileFormat struct {
- Version string `yaml:"version"`
- Actions map[string]ActionCacheEntry `yaml:"actions"`
- Containers map[string]ContainerPinEntry `yaml:"containers,omitempty"`
+ Version string `json:"version"`
+ Actions map[string]ActionCacheEntry `json:"actions"`
+ Containers map[string]ContainerPinEntry `json:"containers,omitempty"`
}
// legacyActionsLockFormat is the on-disk representation of the old actions-lock.json.
@@ -74,7 +73,7 @@ func NewActionCache(repoRoot string) *ActionCache {
}
// Load loads the cache from disk.
-// It first tries the new YAML format at .github/workflows/aw-lock.yml.
+// It first tries the new JSON format at .github/workflows/aw-lock.json.
// If that file does not exist, it falls back to the legacy JSON format at
// .github/aw/actions-lock.json for backward compatibility.
func (c *ActionCache) Load() error {
@@ -102,8 +101,8 @@ func (c *ActionCache) Load() error {
}
var lf awLockFileFormat
- if err := yaml.Unmarshal(data, &lf); err != nil {
- actionCacheLog.Printf("Failed to unmarshal YAML cache data: %v", err)
+ if err := json.Unmarshal(data, &lf); err != nil {
+ actionCacheLog.Printf("Failed to unmarshal JSON cache data: %v", err)
return err
}
@@ -137,7 +136,7 @@ func (c *ActionCache) loadLegacyJSON(data []byte) error {
}
// legacyCachePath derives the old .github/aw/actions-lock.json path from
-// the new .github/workflows/aw-lock.yml path.
+// the new .github/workflows/aw-lock.json path.
func legacyCachePath(newPath string) string {
dir := filepath.Dir(newPath) // .github/workflows
repoRoot := filepath.Dir(dir) // .github
@@ -183,14 +182,14 @@ func (c *ActionCache) Save() error {
return err
}
- // Marshal with sorted entries in YAML format
- data, err := c.marshalSortedYAML()
+ // Marshal with sorted entries in JSON format
+ data, err := c.marshalSortedJSON()
if err != nil {
actionCacheLog.Printf("Failed to marshal cache data: %v", err)
return err
}
- // Add trailing newline
+ // Add trailing newline for prettier compliance
data = append(data, '\n')
if err := os.WriteFile(c.path, data, 0644); err != nil {
@@ -203,8 +202,8 @@ func (c *ActionCache) Save() error {
return nil
}
-// marshalSortedYAML marshals the cache as YAML with sorted action entries.
-func (c *ActionCache) marshalSortedYAML() ([]byte, error) {
+// marshalSortedJSON marshals the cache as JSON with sorted action entries.
+func (c *ActionCache) marshalSortedJSON() ([]byte, error) {
// Sort action keys
actionKeys := make([]string, 0, len(c.Entries))
for key := range c.Entries {
@@ -212,72 +211,60 @@ func (c *ActionCache) marshalSortedYAML() ([]byte, error) {
}
sort.Strings(actionKeys)
- var sb strings.Builder
- sb.WriteString("version: \"")
- sb.WriteString(awLockFileVersion)
- sb.WriteString("\"\n")
- sb.WriteString("actions:\n")
+ // Sort container keys
+ containerKeys := make([]string, 0, len(c.Containers))
+ for key := range c.Containers {
+ containerKeys = append(containerKeys, key)
+ }
+ sort.Strings(containerKeys)
- for _, key := range actionKeys {
- entry := c.Entries[key]
+ // Build the structure with sorted keys manually so JSON output is sorted.
+ var result []byte
+ result = append(result, []byte("{\n \"version\": \""+awLockFileVersion+"\",\n \"actions\": {\n")...)
- // Marshal the entry fields as YAML.
- entryBytes, err := yaml.Marshal(entry)
+ for i, key := range actionKeys {
+ entry := c.Entries[key]
+ entryJSON, err := json.MarshalIndent(entry, " ", " ")
if err != nil {
return nil, fmt.Errorf("marshaling action entry %q: %w", key, err)
}
-
- // Write the key (indent 2 spaces, quote if needed for YAML safety).
- sb.WriteString(" ")
- sb.WriteString(yamlQuoteKey(key))
- sb.WriteString(":\n")
-
- // Indent each line of the entry YAML by 4 spaces.
- for line := range strings.SplitSeq(strings.TrimRight(string(entryBytes), "\n"), "\n") {
- sb.WriteString(" ")
- sb.WriteString(line)
- sb.WriteString("\n")
+ result = append(result, []byte(" \""+jsonEscapeString(key)+"\": ")...)
+ result = append(result, entryJSON...)
+ if i < len(actionKeys)-1 {
+ result = append(result, ',')
}
+ result = append(result, '\n')
}
- // Write containers section.
- if len(c.Containers) > 0 {
- containerKeys := make([]string, 0, len(c.Containers))
- for key := range c.Containers {
- containerKeys = append(containerKeys, key)
- }
- sort.Strings(containerKeys)
+ result = append(result, []byte(" }")...)
- sb.WriteString("containers:\n")
- for _, key := range containerKeys {
+ if len(containerKeys) > 0 {
+ result = append(result, []byte(",\n \"containers\": {\n")...)
+ for i, key := range containerKeys {
entry := c.Containers[key]
- entryBytes, err := yaml.Marshal(entry)
+ entryJSON, err := json.MarshalIndent(entry, " ", " ")
if err != nil {
return nil, fmt.Errorf("marshaling container entry %q: %w", key, err)
}
- sb.WriteString(" ")
- sb.WriteString(yamlQuoteKey(key))
- sb.WriteString(":\n")
- for line := range strings.SplitSeq(strings.TrimRight(string(entryBytes), "\n"), "\n") {
- sb.WriteString(" ")
- sb.WriteString(line)
- sb.WriteString("\n")
+ result = append(result, []byte(" \""+jsonEscapeString(key)+"\": ")...)
+ result = append(result, entryJSON...)
+ if i < len(containerKeys)-1 {
+ result = append(result, ',')
}
+ result = append(result, '\n')
}
+ result = append(result, []byte(" }")...)
}
- return []byte(sb.String()), nil
+ result = append(result, []byte("\n}")...)
+ return result, nil
}
-// yamlQuoteKey returns a YAML-safe key string, quoting it if it contains
-// characters that require quoting (e.g. '@', '/', ':').
-func yamlQuoteKey(key string) string {
- needsQuoting := strings.ContainsAny(key, "@/:[]{}#&*?|<>=!%,`\"'\\")
- if needsQuoting {
- escaped := strings.ReplaceAll(key, "'", "''")
- return "'" + escaped + "'"
- }
- return key
+// jsonEscapeString returns a JSON-safe string (escaping backslash and double-quote).
+func jsonEscapeString(s string) string {
+ s = strings.ReplaceAll(s, `\`, `\\`)
+ s = strings.ReplaceAll(s, `"`, `\"`)
+ return s
}
// Delete removes the cache entry for the given repo and version.
diff --git a/pkg/workflow/action_cache_test.go b/pkg/workflow/action_cache_test.go
index 7d3ba193020..58d33062fe5 100644
--- a/pkg/workflow/action_cache_test.go
+++ b/pkg/workflow/action_cache_test.go
@@ -3,12 +3,12 @@
package workflow
import (
+ "encoding/json"
"os"
"path/filepath"
"testing"
"github.com/github/gh-aw/pkg/testutil"
- "go.yaml.in/yaml/v3"
)
func TestActionCache(t *testing.T) {
@@ -179,11 +179,11 @@ func TestActionCacheSortedEntries(t *testing.T) {
lastPos = pos
}
- // Also verify the file is valid YAML
+ // Also verify the file is valid JSON
var loadedFile awLockFileFormat
- err = yaml.Unmarshal(data, &loadedFile)
+ err = json.Unmarshal(data, &loadedFile)
if err != nil {
- t.Fatalf("Saved cache is not valid YAML: %v", err)
+ t.Fatalf("Saved cache is not valid JSON: %v", err)
}
// Verify all entries are present
diff --git a/scratchpad/debugging-action-pinning.md b/scratchpad/debugging-action-pinning.md
index 742a7f5bda2..bf06d8580f0 100644
--- a/scratchpad/debugging-action-pinning.md
+++ b/scratchpad/debugging-action-pinning.md
@@ -69,17 +69,17 @@ DEBUG=workflow:action_* gh aw compile 2> debug.log
### 2. Inspect the Action Cache
-The action cache is stored at `.github/workflows/aw-lock.yml`. Examine it for duplicate entries:
+The action cache is stored at `.github/workflows/aw-lock.json`. Examine it for duplicate entries:
```bash
# Pretty-print the cache
-cat .github/workflows/aw-lock.yml | jq .
+cat .github/workflows/aw-lock.json | jq .
# Find entries for a specific action
-cat .github/workflows/aw-lock.yml | jq '.entries | to_entries[] | select(.value.repo == "actions/github-script")'
+cat .github/workflows/aw-lock.json | jq '.entries | to_entries[] | select(.value.repo == "actions/github-script")'
# Check for duplicate SHAs
-cat .github/workflows/aw-lock.yml | jq -r '.entries | to_entries[] | "\(.value.sha) \(.key)"' | sort | uniq -d -w 40
+cat .github/workflows/aw-lock.json | jq -r '.entries | to_entries[] | "\(.value.sha) \(.key)"' | sort | uniq -d -w 40
```
### 3. Check for Version Aliases
@@ -120,7 +120,7 @@ workflow:action_pins Dynamic resolution succeeded: actions/github-script@v8 →
#### Cache Operations
```
-workflow:action_cache Loading action cache from: .github/workflows/aw-lock.yml
+workflow:action_cache Loading action cache from: .github/workflows/aw-lock.json
workflow:action_cache Successfully loaded cache with 15 entries
workflow:action_cache Setting cache entry: key=actions/github-script@v8, sha=ed597411...
workflow:action_cache Deduplicating: keeping actions/github-script@v8.0.0, removing actions/github-script@v8
@@ -176,7 +176,7 @@ If you suspect cache corruption, clear it and recompile:
```bash
# Remove the cache file
-rm .github/workflows/aw-lock.yml
+rm .github/workflows/aw-lock.json
# Recompile all workflows
gh aw compile
@@ -223,7 +223,7 @@ If you're using shared workflows that reference actions differently than your lo
CI environments may use a fresh cache on each run, while local development persists the cache:
1. CI may show different version comments than local
-2. Solution: Commit `.github/workflows/aw-lock.yml` to version control
+2. Solution: Commit `.github/workflows/aw-lock.json` to version control
3. This ensures consistent resolution across environments
### Scenario 3: Upstream Version Changes
@@ -296,10 +296,10 @@ All workflow files must use full semantic versioning for actions:
### 2. Commit the Action Cache
-Add `.github/workflows/aw-lock.yml` to version control:
+Add `.github/workflows/aw-lock.json` to version control:
```bash
-git add .github/workflows/aw-lock.yml
+git add .github/workflows/aw-lock.json
git commit -m "chore: add action cache for consistent pinning"
```
@@ -311,9 +311,9 @@ Add to your maintenance workflow:
```bash
# Monthly: clear and regenerate cache
-rm .github/workflows/aw-lock.yml
+rm .github/workflows/aw-lock.json
gh aw compile
-git add .github/workflows/aw-lock.yml
+git add .github/workflows/aw-lock.json
git commit -m "chore: refresh action cache"
```
@@ -356,13 +356,13 @@ Track cache evolution across compiles:
```bash
# Before
-cp .github/workflows/aw-lock.yml before.json
+cp .github/workflows/aw-lock.json before.json
# Compile
gh aw compile
# After
-cp .github/workflows/aw-lock.yml after.json
+cp .github/workflows/aw-lock.json after.json
# Compare
diff -u before.json after.json
@@ -374,10 +374,10 @@ Check for unexpected cache entries:
```bash
# List all SHAs with their version tags
-jq -r '.entries | to_entries[] | "\(.value.sha) \(.value.version) \(.key)"' .github/workflows/aw-lock.yml | sort
+jq -r '.entries | to_entries[] | "\(.value.sha) \(.value.version) \(.key)"' .github/workflows/aw-lock.json | sort
# Find duplicate SHAs
-jq -r '.entries | to_entries[] | .value.sha' .github/workflows/aw-lock.yml | sort | uniq -d
+jq -r '.entries | to_entries[] | .value.sha' .github/workflows/aw-lock.json | sort | uniq -d
```
## Related Documentation
@@ -389,10 +389,10 @@ jq -r '.entries | to_entries[] | .value.sha' .github/workflows/aw-lock.yml | sor
## Troubleshooting Checklist
- [ ] Enabled debug logging: `DEBUG=workflow:action_* gh aw compile`
-- [ ] Checked `.github/workflows/aw-lock.yml` for duplicate entries
+- [ ] Checked `.github/workflows/aw-lock.json` for duplicate entries
- [ ] Verified version tags point to same SHA via GitHub API
- [ ] Searched workflows for inconsistent version formats
-- [ ] Cleared cache and recompiled: `rm .github/workflows/aw-lock.yml && gh aw compile`
+- [ ] Cleared cache and recompiled: `rm .github/workflows/aw-lock.json && gh aw compile`
- [ ] Checked for upstream version tag changes
- [ ] Reviewed action_pins.json for canonical versions
- [ ] Consulted team on preferred version format
@@ -404,7 +404,7 @@ jq -r '.entries | to_entries[] | .value.sha' .github/workflows/aw-lock.yml | sor
If the issue persists after following this guide:
1. Capture debug logs: `DEBUG=workflow:action_* gh aw compile 2> debug.log`
-2. Export cache state: `cat .github/workflows/aw-lock.yml > cache.json`
+2. Export cache state: `cat .github/workflows/aw-lock.json > cache.json`
3. List workflow action references: `grep -r "uses: " .github/workflows/ > actions.txt`
4. Create an issue with these artifacts attached
diff --git a/scratchpad/layout.md b/scratchpad/layout.md
index 1a4d57fe126..f4a4ca40591 100644
--- a/scratchpad/layout.md
+++ b/scratchpad/layout.md
@@ -121,7 +121,7 @@ Common file paths referenced in workflow files:
| Path | Type | Description | Usage Context |
|------|------|-------------|---------------|
| `.github/workflows/` | Directory | Workflow definition directory | Contains all `.md` and `.lock.yml` workflow files |
-| `.github/aw/` | Directory | Agentic workflow configuration | Contains `aw-lock.yml` and other configs |
+| `.github/aw/` | Directory | Agentic workflow configuration | Contains `aw-lock.json` and other configs |
| `.github/agents/` | Directory | Custom agent definitions | Contains agent markdown files (e.g., `test-agent.md`) |
| `/tmp/gh-aw/` | Directory | Temporary workflow data | Root temporary directory for all workflow artifacts |
| `/tmp/gh-aw/agent/` | Directory | Agent execution workspace | Agent's working directory during execution |
@@ -382,7 +382,7 @@ GitHub Actions runner images used across compiled workflows:
├── agents/ # Custom agent definitions
│ └── test-agent.md
├── aw/ # Workflow configuration
-│ └── aw-lock.yml
+│ └── aw-lock.json
└── workflows/ # Workflow files
├── *.md # Source workflows
├── *.lock.yml # Compiled workflows
diff --git a/scripts/sync-action-pins/main.go b/scripts/sync-action-pins/main.go
index 919afe76fd8..b8e7f06508b 100644
--- a/scripts/sync-action-pins/main.go
+++ b/scripts/sync-action-pins/main.go
@@ -1,4 +1,4 @@
-// sync-action-pins converts .github/workflows/aw-lock.yml into
+// sync-action-pins converts .github/workflows/aw-lock.json (JSON) into
// pkg/workflow/data/action_pins.json (the embedded JSON fallback used by the
// compiler for SHA pinning when the GitHub API is unavailable).
//
@@ -30,7 +30,7 @@ func main() {
Inputs map[string]*workflow.ActionYAMLInput `json:"inputs,omitempty"`
// ActionDescription is intentionally omitted: action_pins.json is the embedded
// fallback used only for SHA pinning; descriptions are only needed at compile time
- // from the user-facing aw-lock.yml cache and are not part of ActionPin.
+ // from the user-facing aw-lock.json cache and are not part of ActionPin.
}
type actionPinsData struct {
Entries map[string]actionPin `json:"entries"`
From 830862e37632e68ec42e56f5ae35a8dc903ce320 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 7 Apr 2026 15:52:50 +0000
Subject: [PATCH 4/4] fix: address review comments - update stale aw-lock.yml
refs, jq filters, layout doc, safe migration
- pkg/workflow/safe_outputs_actions.go: fix two comments still saying 'aw-lock.yml'
- pkg/workflow/compiler_types.go: fix comment saying 'aw-lock.yml'
- scratchpad/debugging-action-pinning.md: update jq filters from .entries to .actions (x4)
- scratchpad/layout.md: update table and directory tree to show aw-lock.json in .github/workflows/
- pkg/cli/codemod_actions_lock_migration.go: when new file already exists, skip migration instead of deleting the legacy file
- scripts/sync-action-pins/main.go: fail with error when no source cache file is found (instead of silently writing empty action_pins.json)
Agent-Logs-Url: https://github.com/github/gh-aw/sessions/1c9e14dd-0ab3-48a8-8390-a4ce974fd972
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
---
pkg/cli/codemod_actions_lock_migration.go | 11 ++++------
pkg/workflow/compiler_types.go | 2 +-
pkg/workflow/safe_outputs_actions.go | 4 ++--
scratchpad/debugging-action-pinning.md | 8 ++++----
scratchpad/layout.md | 9 ++++----
scripts/sync-action-pins/main.go | 25 +++++++++++++++++++++--
6 files changed, 39 insertions(+), 20 deletions(-)
diff --git a/pkg/cli/codemod_actions_lock_migration.go b/pkg/cli/codemod_actions_lock_migration.go
index aad6b6cd37b..d1ef5459de5 100644
--- a/pkg/cli/codemod_actions_lock_migration.go
+++ b/pkg/cli/codemod_actions_lock_migration.go
@@ -50,15 +50,12 @@ func MigrateActionsLockFile(write bool, verbose bool) (bool, error) {
return true, nil
}
- // If the new file already exists, skip migration to avoid overwriting.
+ // If the new file already exists, skip migration to avoid overwriting or
+ // discarding the legacy file without verifying the contents match.
if _, err := os.Stat(newPath); err == nil {
- // Both files exist: warn and remove the legacy file.
fmt.Fprintf(os.Stderr, "%s\n", console.FormatWarningMessage(
- fmt.Sprintf("%s already exists; removing legacy %s", newPath, legacyPath)))
- if err := os.Remove(legacyPath); err != nil {
- return false, fmt.Errorf("removing legacy %s: %w", legacyPath, err)
- }
- return true, nil
+ fmt.Sprintf("%s already exists; leaving legacy %s in place and skipping migration", newPath, legacyPath)))
+ return false, nil
}
// Load via ActionCache (which handles the legacy JSON format) and re-save
diff --git a/pkg/workflow/compiler_types.go b/pkg/workflow/compiler_types.go
index 4a5a5be180b..69d61eedf88 100644
--- a/pkg/workflow/compiler_types.go
+++ b/pkg/workflow/compiler_types.go
@@ -96,7 +96,7 @@ func NewCompiler(opts ...CompilerOption) *Compiler {
version := GetVersion()
// Auto-detect git repository root for action cache path resolution
- // This ensures aw-lock.yml is created at repo root regardless of CWD
+ // This ensures aw-lock.json is created at repo root regardless of CWD
gitRoot := findGitRoot()
// Create compiler with defaults
diff --git a/pkg/workflow/safe_outputs_actions.go b/pkg/workflow/safe_outputs_actions.go
index 572208c5f0f..efbf34a6b71 100644
--- a/pkg/workflow/safe_outputs_actions.go
+++ b/pkg/workflow/safe_outputs_actions.go
@@ -166,7 +166,7 @@ func parseActionUsesField(uses string) (*actionRef, error) {
//
// Resolution priority (highest wins):
// 1. Inputs already specified in the frontmatter (config.Inputs != nil)
-// 2. Inputs cached in the ActionCache (aw-lock.yml)
+// 2. Inputs cached in the ActionCache (aw-lock.json)
// 3. Inputs fetched from the remote action.yml (result cached for future runs)
//
// When available, the action reference is pinned to a commit SHA for security;
@@ -220,7 +220,7 @@ func (c *Compiler) fetchAndParseActionYAML(actionName string, config *SafeOutput
if !inputsFromFrontmatter {
// Check the ActionCache for previously-fetched inputs before going to the network.
// The cache key uses the original version tag from the `uses:` field (ref.Ref, e.g.
- // "v1") which matches the key stored in aw-lock.yml.
+ // "v1") which matches the key stored in aw-lock.json.
if data.ActionCache != nil {
if cachedInputs, ok := data.ActionCache.GetInputs(ref.Repo, ref.Ref); ok {
safeOutputActionsLog.Printf("Using cached inputs for %q (%s@%s)", actionName, ref.Repo, ref.Ref)
diff --git a/scratchpad/debugging-action-pinning.md b/scratchpad/debugging-action-pinning.md
index bf06d8580f0..95c8fa7f755 100644
--- a/scratchpad/debugging-action-pinning.md
+++ b/scratchpad/debugging-action-pinning.md
@@ -76,10 +76,10 @@ The action cache is stored at `.github/workflows/aw-lock.json`. Examine it for d
cat .github/workflows/aw-lock.json | jq .
# Find entries for a specific action
-cat .github/workflows/aw-lock.json | jq '.entries | to_entries[] | select(.value.repo == "actions/github-script")'
+cat .github/workflows/aw-lock.json | jq '.actions | to_entries[] | select(.value.repo == "actions/github-script")'
# Check for duplicate SHAs
-cat .github/workflows/aw-lock.json | jq -r '.entries | to_entries[] | "\(.value.sha) \(.key)"' | sort | uniq -d -w 40
+cat .github/workflows/aw-lock.json | jq -r '.actions | to_entries[] | "\(.value.sha) \(.key)"' | sort | uniq -d -w 40
```
### 3. Check for Version Aliases
@@ -374,10 +374,10 @@ Check for unexpected cache entries:
```bash
# List all SHAs with their version tags
-jq -r '.entries | to_entries[] | "\(.value.sha) \(.value.version) \(.key)"' .github/workflows/aw-lock.json | sort
+jq -r '.actions | to_entries[] | "\(.value.sha) \(.value.version) \(.key)"' .github/workflows/aw-lock.json | sort
# Find duplicate SHAs
-jq -r '.entries | to_entries[] | .value.sha' .github/workflows/aw-lock.json | sort | uniq -d
+jq -r '.actions | to_entries[] | .value.sha' .github/workflows/aw-lock.json | sort | uniq -d
```
## Related Documentation
diff --git a/scratchpad/layout.md b/scratchpad/layout.md
index f4a4ca40591..ddf6ce6b081 100644
--- a/scratchpad/layout.md
+++ b/scratchpad/layout.md
@@ -120,8 +120,8 @@ Common file paths referenced in workflow files:
| Path | Type | Description | Usage Context |
|------|------|-------------|---------------|
-| `.github/workflows/` | Directory | Workflow definition directory | Contains all `.md` and `.lock.yml` workflow files |
-| `.github/aw/` | Directory | Agentic workflow configuration | Contains `aw-lock.json` and other configs |
+| `.github/workflows/` | Directory | Workflow definition directory | Contains all `.md`, `.lock.yml` workflow files, and `aw-lock.json` |
+| `.github/aw/` | Directory | Agentic workflow configuration | Contains other configs and legacy `.github/aw/actions-lock.json` path |
| `.github/agents/` | Directory | Custom agent definitions | Contains agent markdown files (e.g., `test-agent.md`) |
| `/tmp/gh-aw/` | Directory | Temporary workflow data | Root temporary directory for all workflow artifacts |
| `/tmp/gh-aw/agent/` | Directory | Agent execution workspace | Agent's working directory during execution |
@@ -381,11 +381,12 @@ GitHub Actions runner images used across compiled workflows:
.github/
├── agents/ # Custom agent definitions
│ └── test-agent.md
-├── aw/ # Workflow configuration
-│ └── aw-lock.json
+├── aw/ # Legacy workflow configuration
+│ └── actions-lock.json # Legacy (migrated to .github/workflows/aw-lock.json)
└── workflows/ # Workflow files
├── *.md # Source workflows
├── *.lock.yml # Compiled workflows
+ ├── aw-lock.json # Canonical actions lock file
└── shared/ # Shared workflow components
````
diff --git a/scripts/sync-action-pins/main.go b/scripts/sync-action-pins/main.go
index b8e7f06508b..1ecd4df60f7 100644
--- a/scripts/sync-action-pins/main.go
+++ b/scripts/sync-action-pins/main.go
@@ -17,10 +17,31 @@ import (
)
func main() {
+ sourcePaths := []string{
+ ".github/workflows/aw-lock.json",
+ ".github/aw/actions-lock.json",
+ }
+
+ foundSource := false
+ for _, path := range sourcePaths {
+ if _, err := os.Stat(path); err == nil {
+ foundSource = true
+ break
+ } else if !os.IsNotExist(err) {
+ fmt.Fprintf(os.Stderr, "Error checking %s: %v\n", path, err)
+ os.Exit(1)
+ }
+ }
+
+ if !foundSource {
+ fmt.Fprintf(os.Stderr, "Error: no action cache source file found; expected %q or %q\n", sourcePaths[0], sourcePaths[1])
+ os.Exit(1)
+ }
+
cache := workflow.NewActionCache(".")
if err := cache.Load(); err != nil {
- fmt.Fprintf(os.Stderr, "Warning: failed to load action cache: %v\n", err)
- // Continue with empty cache — action_pins.json will be written empty.
+ fmt.Fprintf(os.Stderr, "Error: failed to load action cache: %v\n", err)
+ os.Exit(1)
}
type actionPin struct {