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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/actions.lock
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# This file is machine-generated by `gh actions-pin`.
# Do not edit by hand; run `gh actions-pin` to update.
# This file is machine-generated by `gh actions-lock`.
# Do not edit by hand; run `gh actions-lock` to update.
# Docs: https://gh.io/actions-lockfile
version: 'v0.0.1'
workflows:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
with:
generate_attestations: true
go_version_file: go.mod
go_build_options: ./cmd/gh-actions-pin
go_build_options: ./cmd/gh-actions-lock

sync-early-access-release:
needs: release
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/gh-actions-pin
/gh-actions-pin.exe
/gh-actions-lock
/gh-actions-lock.exe

# VHS demo recordings
/demo/vhs/out/
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
BIN := gh-actions-pin
EXT_NAME := gh-actions-pin
BIN := gh-actions-lock
EXT_NAME := gh-actions-lock
# Honor XDG_DATA_HOME so this matches where gh actually resolves its data
# dir; fall back to the documented default when it's unset.
XDG_DATA_HOME ?= $(HOME)/.local/share
Expand All @@ -10,7 +10,7 @@ RUBY := $(shell command -v /opt/homebrew/opt/ruby/bin/ruby 2>/dev/null || echo r
.PHONY: build test test-integration test-shell test-live test-matrix test-smoke test-stub test-real install reinstall uninstall

build:
go build -o $(BIN) ./cmd/gh-actions-pin
go build -o $(BIN) ./cmd/gh-actions-lock

test:
go test ./...
Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# gh-actions-pin
# gh-actions-lock

Manage your workflow dependencies.

## Install

```bash
gh extension install github/gh-actions-pin
gh extension install github/gh-actions-lock
Comment thread
nodeselector marked this conversation as resolved.
```

## Usage
Expand All @@ -14,27 +14,27 @@ Scan every workflow under `.github/workflows/` and pin what it can -- pinning
each resolvable action to an immutable SHA and updating the lockfile:

```bash
gh actions-pin
gh actions-lock
```

Scope the scan to a single workflow (same default behavior, one file):

```bash
gh actions-pin .github/workflows/ci.yml
gh actions-lock .github/workflows/ci.yml
```

By default, already-pinned workflows are trusted from the lockfile -- their
reachability isn't re-checked against upstream. To force a full re-verification
of every recorded pin (bypassing that fast path):

```bash
gh actions-pin --rescan
gh actions-lock --rescan
```

Read-only check for CI (reports findings, writes nothing):

```bash
gh actions-pin --no-fix --json=valid,findings
gh actions-lock --no-fix --json=valid,findings
```

`--no-fix` controls whether fixes are applied; `--json` only selects the output
Expand All @@ -45,12 +45,12 @@ format. Structured results go to stdout, progress to stderr.
GitHub Actions is a package manager that forgot to ship a lockfile. Your
workflows are the manifest -- every `uses:` line is a dependency, resolved by
mutable tag or branch *at runtime*, on GitHub's servers, with no record of what
actually ran. `gh-actions-pin` supplies the missing half: `.github/workflows/actions.lock`,
actually ran. `gh-actions-lock` supplies the missing half: `.github/workflows/actions.lock`,
the Actions analogue of `go.sum` or `package-lock.json`. Each run resolves every
direct and transitive dependency to an immutable commit SHA, locks it, and
verifies the lock hasn't been tampered with before any of it runs.

A single `gh actions-pin` invocation walks two paths. The **verify** path is
A single `gh actions-lock` invocation walks two paths. The **verify** path is
read-only and always runs: it scans every workflow, resolves each dependency to
a commit SHA, and checks the result against the lockfile. The **fix** path
applies pins — rewriting `uses:` lines and updating the lockfile — for the
Expand All @@ -59,7 +59,7 @@ past in the spinner.

```mermaid
flowchart TD
Start([gh actions-pin]) --> Scan
Start([gh actions-lock]) --> Scan

subgraph verify["VERIFY · read-only diagnosis"]
direction TB
Expand Down Expand Up @@ -88,7 +88,7 @@ flowchart TD
The security guarantee lives in **Verifying reachability**: a SHA pin is only
trustworthy if that commit is reachable from the tag/branch it claims to come
from. A SHA that resolves but isn't in the ref's history is an *impostor commit*
-- the fork-network attack `gh-actions-pin` exists to catch -- and it's flagged
-- the fork-network attack `gh-actions-lock` exists to catch -- and it's flagged
rather than silently trusted.

## Development
Expand All @@ -114,6 +114,6 @@ sides pick them up.
| Variable | Purpose |
|---|---|
| `GH_TOKEN` / `GITHUB_TOKEN` | Auth token for live tests (falls back to `gh auth token`) |
| `GH_ACTIONS_PIN_WORKFLOWS_DIR` | Override the workflows directory to scan (lab/testing use) |
| `GH_ACTIONS_LOCK_WORKFLOWS_DIR` | Override the workflows directory to scan (lab/testing use) |
| `KEEP_FIXTURES` | Keep temp dirs after test runs for debugging |

34 changes: 17 additions & 17 deletions cmd/gh-actions-pin/check.go → cmd/gh-actions-lock/check.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Command gh-actions-pin scans workflows and pins GitHub Actions to
// Command gh-actions-lock scans workflows and pins GitHub Actions to
// immutable commit SHAs.
package main

Expand All @@ -14,16 +14,16 @@ import (
"github.com/MakeNowJust/heredoc"
"github.com/cli/go-gh/v2/pkg/repository"
parserlock "github.com/github/actions-lockfile/go/pkg/lockfile"
"github.com/github/gh-actions-pin/cmd/gh-actions-pin/format"
"github.com/github/gh-actions-pin/internal/config"
"github.com/github/gh-actions-pin/internal/pin"
"github.com/github/gh-actions-pin/internal/pinpool"
"github.com/github/gh-actions-pin/internal/pipeline"
"github.com/github/gh-actions-pin/internal/pipeline/checks"
"github.com/github/gh-actions-pin/internal/profile"
"github.com/github/gh-actions-pin/internal/resolve"
"github.com/github/gh-actions-pin/internal/tag"
"github.com/github/gh-actions-pin/internal/ui"
"github.com/github/gh-actions-lock/cmd/gh-actions-lock/format"
"github.com/github/gh-actions-lock/internal/config"
"github.com/github/gh-actions-lock/internal/pin"
"github.com/github/gh-actions-lock/internal/pinpool"
"github.com/github/gh-actions-lock/internal/pipeline"
"github.com/github/gh-actions-lock/internal/pipeline/checks"
"github.com/github/gh-actions-lock/internal/profile"
"github.com/github/gh-actions-lock/internal/resolve"
"github.com/github/gh-actions-lock/internal/tag"
"github.com/github/gh-actions-lock/internal/ui"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -70,7 +70,7 @@ func newCheckCmd(newResolver resolverFunc) *cobra.Command {
--json selects the output format only — structured results on
stdout, progress on stderr — and is independent of --no-fix:

gh actions-pin check --no-fix --json 2>/dev/null | jq .valid
gh actions-lock check --no-fix --json 2>/dev/null | jq .valid

Issue types:
ref-moved - locked SHA no longer matches upstream (expected for mutable tags like v4)
Expand All @@ -95,16 +95,16 @@ func newCheckCmd(newResolver resolverFunc) *cobra.Command {
`),
Example: heredoc.Doc(`
# Verify all workflows and fix what's fixable
$ gh actions-pin check
$ gh actions-lock check

# Verify a specific workflow
$ gh actions-pin check .github/workflows/ci.yml
$ gh actions-lock check .github/workflows/ci.yml

# Read-only check for CI (writes nothing, exits 1 if invalid)
$ gh actions-pin check --no-fix --json=valid,findings
$ gh actions-lock check --no-fix --json=valid,findings

# All fields as JSON
$ gh actions-pin check --json
$ gh actions-lock check --json
`),
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
Expand Down Expand Up @@ -469,7 +469,7 @@ func injectVersionRefFindings(report *checks.Report, record *pin.Record) {
}
}

// cliVersion returns the gh-actions-pin extension version embedded by the Go
// cliVersion returns the gh-actions-lock extension version embedded by the Go
// build system. Returns "(devel)" for local `go build` and a real version
// like "v0.1.2" when installed via `gh extension install`.
func cliVersion() string {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package main

// Golden-file snapshot test for the `gh actions-pin check --json` contract.
// Golden-file snapshot test for the `gh actions-lock check --json` contract.
//
// We promised Dependabot the JSON shape is additive-only: no field renames,
// no removals, no type shifts. New optional fields are allowed. This test
// enforces that promise structurally.
//
// To regenerate the golden after an intentional additive change:
//
// UPDATE_GOLDEN=1 go test ./cmd/gh-actions-pin/ -run TestCheckCommand_JSONGolden
// UPDATE_GOLDEN=1 go test ./cmd/gh-actions-lock/ -run TestCheckCommand_JSONGolden
//
// CI runs without the env var, so any change to the JSON shape must be
// intentional and committed alongside the code change.
Expand All @@ -23,7 +23,7 @@ import (
"strings"
"testing"

"github.com/github/gh-actions-pin/internal/ghapi/httpmock"
"github.com/github/gh-actions-lock/internal/ghapi/httpmock"
"github.com/stretchr/testify/require"
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (
"strings"
"testing"

"github.com/github/gh-actions-pin/cmd/gh-actions-pin/format"
"github.com/github/gh-actions-pin/internal/ghapi/httpmock"
"github.com/github/gh-actions-pin/internal/pinpool"
"github.com/github/gh-actions-pin/internal/resolve"
"github.com/github/gh-actions-lock/cmd/gh-actions-lock/format"
"github.com/github/gh-actions-lock/internal/ghapi/httpmock"
"github.com/github/gh-actions-lock/internal/pinpool"
"github.com/github/gh-actions-lock/internal/resolve"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"io"
"strings"

"github.com/github/gh-actions-pin/internal/pipeline/checks"
"github.com/github/gh-actions-lock/internal/pipeline/checks"
)

// validJSONField reports whether name is a recognized --json output field.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import (
"fmt"
"strings"

"github.com/github/gh-actions-pin/internal/pipeline/checks"
"github.com/github/gh-actions-lock/internal/pipeline/checks"

"github.com/github/gh-actions-pin/internal/pipeline"
"github.com/github/gh-actions-pin/internal/ui"
"github.com/github/gh-actions-lock/internal/pipeline"
"github.com/github/gh-actions-lock/internal/ui"
)

// PresentResults renders human-readable output from a check report.
Expand Down Expand Up @@ -200,8 +200,8 @@ func renderWarnings(out *ui.UI, report *checks.Report, willRemediate bool) {
bareSHADeps = append(bareSHADeps, key)
}
case f.Category == checks.RefMoved:
// TODO: surface ref-moved warnings once the `gh actions-pin
// update` path exists. Today the guidance ("run gh actions-pin
// TODO: surface ref-moved warnings once the `gh actions-lock
// update` path exists. Today the guidance ("run gh actions-lock
// to update") is wrong — a plain re-run trusts the lockfile and
// repins nothing; only --rescan even detects the movement. Until
// there's a command that actually advances a moved ref, swallow
Expand All @@ -221,14 +221,14 @@ func renderWarnings(out *ui.UI, report *checks.Report, willRemediate bool) {
if willRemediate {
out.TermDetail("↳ resolving below")
} else {
out.TermDetail("↳ run `gh actions-pin` to pin them")
out.TermDetail("↳ run `gh actions-lock` to pin them")
}
}
if len(bareSHADeps) > 0 && !willRemediate {
out.TermWarn("%d %s pinned to a bare SHA without a tag ref",
len(bareSHADeps),
ui.Pluralize(len(bareSHADeps), "action is", "actions are"))
out.TermDetail("↳ run `gh actions-pin` to pin to tagged releases")
out.TermDetail("↳ run `gh actions-lock` to pin to tagged releases")
}
for _, key := range otherDetailWarnings {
wg := warnMap[key]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import (
"strings"
"testing"

"github.com/github/gh-actions-pin/internal/pipeline/checks"
"github.com/github/gh-actions-lock/internal/pipeline/checks"

parserlock "github.com/github/actions-lockfile/go/pkg/lockfile"
"github.com/github/gh-actions-pin/internal/dep"
"github.com/github/gh-actions-pin/internal/ui"
"github.com/github/gh-actions-lock/internal/dep"
"github.com/github/gh-actions-lock/internal/ui"
)

// newTestUI returns a UI whose narration log sinks to io.Discard, mirroring
Expand Down Expand Up @@ -52,7 +52,7 @@ func TestPresentResults_WarningsReachTerminal(t *testing.T) {
notWanted: []string{
"moved upstream",
"compare/111111111111...222222222222",
"run `gh actions-pin` to update",
"run `gh actions-lock` to update",
},
},
{
Expand Down Expand Up @@ -176,7 +176,7 @@ func TestPresentResults_RemediateHints(t *testing.T) {
Confidence: checks.ConfidenceHigh,
}},
wantOutput: []string{"not yet pinned", "↳ resolving below"},
notWanted: []string{"run `gh actions-pin`"},
notWanted: []string{"run `gh actions-lock`"},
},
{
name: "not-pinned shows manual hint when not remediating",
Expand All @@ -187,7 +187,7 @@ func TestPresentResults_RemediateHints(t *testing.T) {
Severity: checks.SeverityWarning,
Confidence: checks.ConfidenceHigh,
}},
wantOutput: []string{"not yet pinned", "↳ run `gh actions-pin` to pin them"},
wantOutput: []string{"not yet pinned", "↳ run `gh actions-lock` to pin them"},
notWanted: []string{"resolving below"},
},
{
Expand Down Expand Up @@ -225,7 +225,7 @@ func TestPresentResults_RemediateHints(t *testing.T) {
}},
notWanted: []string{
"moved upstream",
"run `gh actions-pin` to update",
"run `gh actions-lock` to update",
},
},
{
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"os"

"github.com/cli/go-gh/v2/pkg/prompter"
"github.com/github/gh-actions-pin/internal/ui"
"github.com/github/gh-actions-lock/internal/ui"
"github.com/spf13/cobra"
"golang.org/x/term"
)
Expand Down Expand Up @@ -67,7 +67,7 @@ type lockRecovery func(lockPath string, parseErr error) (recovered bool, err err
func newLockRecovery(noInteractive bool, console *ui.UI, newConfirm confirmFactory, allowDelete bool) lockRecovery {
return func(lockPath string, parseErr error) (bool, error) {
if !allowDelete {
return false, fmt.Errorf("%w; run `gh actions-pin check` to rebuild it, or delete it by hand", parseErr)
return false, fmt.Errorf("%w; run `gh actions-lock check` to rebuild it, or delete it by hand", parseErr)
}
var (
confirm confirmer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"path/filepath"
"testing"

"github.com/github/gh-actions-pin/internal/ui"
"github.com/github/gh-actions-lock/internal/ui"
)

type fakeConfirmer struct {
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"fmt"

parserlock "github.com/github/actions-lockfile/go/pkg/lockfile"
"github.com/github/gh-actions-pin/internal/pipeline/checks"
"github.com/github/gh-actions-lock/internal/pipeline/checks"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -33,7 +33,7 @@ func gateNoOnboard(report *checks.Report) []string {
f.Category = checks.OnboardingRequired
f.Severity = checks.SeverityInfo
f.Detail = fmt.Sprintf("%s@%s has no lockfile entry; --no-onboard refuses to add new workflows or actions", ar.FullName(), ar.Ref)
f.Remediation = "onboard it first with `gh actions-pin check` (without --no-onboard)"
f.Remediation = "onboard it first with `gh actions-lock check` (without --no-onboard)"
refused = append(refused, fmt.Sprintf("%s@%s in %s", ar.FullName(), ar.Ref, wr.Path))
}
if len(refusedKeys) == 0 {
Expand Down
Loading
Loading