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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions .github/aw/actions-lock.json → .github/workflows/aw-lock.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"entries": {
"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 }}"
"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.",
Expand All @@ -17,8 +18,8 @@
"description": "The number of the issue or pull request."
},
"repo": {
"description": "The owner and repository name. e.g.) Codertocat/Hello-World",
"default": "${{ github.repository }}"
"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."
Expand Down
14 changes: 5 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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.json 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.json 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
Expand Down Expand Up @@ -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.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)"
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/introduction/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ flowchart TB

subgraph Pinning["Action Pinning"]
SHA["SHA Resolution<br/>actions/checkout@sha # v4"]
CACHE[/"actions-lock.json<br/>(Cached SHAs)"/]
CACHE[/"aw-lock.json<br/>(Cached SHAs)"/]
end

subgraph Scanners["Security Scanners"]
Expand Down
12 changes: 6 additions & 6 deletions docs/src/content/docs/reference/compilation-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.json`) → GitHub API → embedded pins.

### The actions-lock.json Cache
### The aw-lock.json 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.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 `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.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

Expand Down Expand Up @@ -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.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.

Expand All @@ -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.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.

Expand Down
8 changes: 4 additions & 4 deletions docs/src/content/docs/reference/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.json 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.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 `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.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 `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.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`?

Expand Down
6 changes: 3 additions & 3 deletions docs/src/content/docs/reference/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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 actions-lock.json to latest SHAs
gh aw update-actions # Update aw-lock.json to latest SHAs
gh aw compile # Recompile workflows using the refreshed pins
```

Expand All @@ -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.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.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/setup/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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.

Expand Down
83 changes: 83 additions & 0 deletions pkg/cli/codemod_actions_lock_migration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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.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.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).
// 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.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)
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 or
// discarding the legacy file without verifying the contents match.
if _, err := os.Stat(newPath); err == nil {
fmt.Fprintf(os.Stderr, "%s\n", console.FormatWarningMessage(
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
// 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)
}

// 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
}
23 changes: 12 additions & 11 deletions pkg/cli/compile_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading