diff --git a/README.md b/README.md index a58c4de..f744b9f 100644 --- a/README.md +++ b/README.md @@ -114,10 +114,22 @@ A pin key is `OWNER/REPO@REF`. The same key appears in both `workflows` (as flat transitive lists) and `dependencies` (as deduplicated graph entries with `uses:` links to direct dependencies). -The `hostname` field is optional in v0.0.3. Dotcom-only producers may omit it. -Hostname-aware producers running in Proxima record the canonical hostname for -every direct and transitive dependency, including `github.com` dependencies in -mixed graphs. When present, `hostname` must be the bare lowercase `github.com` +The `hostname` field is optional in v0.0.3. An omitted `hostname` binds the +pin to the home host: the tenant (`.ghe.com`) on a GHE.com +data-residency instance, or `github.com` on github.com. + +- Producers must omit `hostname` for pins bound to the home host. The only + value producers write explicitly is `github.com`, for pins bound to + github.com while running on a GHE.com data-residency instance. Producers + never write a `*.ghe.com` hostname. +- On github.com, `hostname` must be omitted or `github.com`. Any other value + is rejected. +- On GHE.com data-residency instances, only v0.0.3 lockfiles are accepted. + v0.0.1 and v0.0.2 lockfiles used omission to mean github.com, so producers + migrate them to v0.0.3 with an explicit `github.com` hostname on every + dependency. + +Syntactically, a present `hostname` must be the bare lowercase `github.com` hostname or a lowercase GHE tenant hostname such as `octocorp.ghe.com`. The parser also reads the dotcom-only v0.0.1 and v0.0.2 lockfiles, defaulting diff --git a/go/pkg/lockfile/lockfile.go b/go/pkg/lockfile/lockfile.go index 87143ad..b5b8604 100644 --- a/go/pkg/lockfile/lockfile.go +++ b/go/pkg/lockfile/lockfile.go @@ -86,7 +86,6 @@ const CLIName = "gh actions-lock" // - actions/checkout@v6 // dependencies: // actions/checkout@v4.3.1: -// hostname: github.com // ref: v4.3.1 // commit: sha1-34e114876b0b11c390a56381ad16ebd13914f8d5 // owner_id: 44036562 @@ -215,23 +214,24 @@ func (f File) LookupWorkflow(workflowKey string) ([]string, bool) { } // Action carries the per-action metadata recorded under a pin key. -// -// Hostname is the optional bare canonical hostname of the GitHub instance that -// owns the dependency: github.com or a lowercase GHE tenant hostname such as -// octocorp.ghe.com. It is empty when omitted. Ref is the git ref the commit was -// resolved from (required). Commit is the digest in algo-prefixed form (e.g. -// "sha1-abc123...", "sha256-def456...") (required). OwnerID and RepoID are the -// host-specific numeric IDs for the owner and repository, used to detect a -// repository transfer (the name changes but the ID does not). Uses lists the -// action's direct nested dependencies as canonical pin keys — empty for leaf -// actions, populated for composite actions. type Action struct { - Hostname string `yaml:"hostname,omitempty"` - Ref string `yaml:"ref,omitempty"` - Commit string `yaml:"commit,omitempty"` - OwnerID int64 `yaml:"owner_id"` - RepoID int64 `yaml:"repo_id"` - Uses []string `yaml:"uses,omitempty"` + // Hostname is the GitHub instance that owns the dependency. Empty means + // the home host: the tenant on a GHE.com data-residency instance, or + // github.com on github.com. Producers set it only to github.com, for + // github.com-bound pins on a GHE.com data-residency instance. + Hostname string `yaml:"hostname,omitempty"` + // Ref is the git ref the commit was resolved from (required). + Ref string `yaml:"ref,omitempty"` + // Commit is the digest in algo-prefixed form, e.g. "sha1-abc123..." + // (required). + Commit string `yaml:"commit,omitempty"` + // OwnerID and RepoID are the host-specific numeric IDs for the owner and + // repository, used to detect a transfer (the name changes, the ID does not). + OwnerID int64 `yaml:"owner_id"` + RepoID int64 `yaml:"repo_id"` + // Uses lists the action's direct nested dependencies as canonical pin + // keys: empty for leaf actions, populated for composite actions. + Uses []string `yaml:"uses,omitempty"` } // MaxParseSize is the maximum number of bytes Parse accepts. Larger inputs are diff --git a/go/pkg/lockfile/schema_gen.go b/go/pkg/lockfile/schema_gen.go index 46268af..409a4a8 100644 --- a/go/pkg/lockfile/schema_gen.go +++ b/go/pkg/lockfile/schema_gen.go @@ -6,4 +6,4 @@ const schemaV001 = "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/sc const schemaV002 = "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://gh.io/actions-lockfile/v0.0.2.json\",\n \"title\": \"GitHub Actions dependency lockfile\",\n \"description\": \"Machine-generated lockfile describing the pinned action dependency graph for a repository's workflows. Written and updated by `gh actions-lock`.\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"version\"],\n \"properties\": {\n \"version\": {\n \"description\": \"Lockfile schema version. Only v0.0.2 is supported.\",\n \"const\": \"v0.0.2\"\n },\n \"workflows\": {\n \"description\": \"Map of repo-relative workflow path to the flat, transitive list of canonical pin keys it depends on.\",\n \"type\": \"object\",\n \"additionalProperties\": {\n \"type\": \"array\",\n \"items\": { \"$ref\": \"#/$defs/pin\" }\n }\n },\n \"dependencies\": {\n \"description\": \"Deduplicated action graph keyed by canonical pin. Each entry records the resolved metadata for one action tarball.\",\n \"type\": \"object\",\n \"additionalProperties\": { \"$ref\": \"#/$defs/action\" }\n }\n },\n \"$defs\": {\n \"pin\": {\n \"description\": \"Canonical dependency pin: OWNER/REPO@REF (e.g. actions/checkout@v4).\",\n \"type\": \"string\",\n \"pattern\": \"^[^/@:]+/[^/@:]+@[^:]+$\"\n },\n \"action\": {\n \"description\": \"Resolved metadata for a single pinned action.\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"ref\", \"commit\", \"owner_id\", \"repo_id\"],\n \"properties\": {\n \"ref\": {\n \"description\": \"The git ref the commit was resolved from. Required: every dep that passes impostor checks has a resolvable ref. The CLI picks the best ref with priority: full semver tag > any tag > branch (protected > default > release/v* > any). The parser enforces presence; priority ordering is the CLI's concern.\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"commit\": {\n \"description\": \"The exact commit the action resolves to, in algo-prefixed digest form (e.g. sha1-...). This is the immutable identity the runner checks out; tags and branches can be moved, this cannot.\",\n \"type\": \"string\"\n },\n \"owner_id\": {\n \"description\": \"The numeric ID of the action's owner (user or org). Pinned because names can be deleted and re-registered by someone else; the ID cannot, so it ties the pin to the original owner.\",\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"repo_id\": {\n \"description\": \"The numeric ID of the action's repository. Pinned because a repo can be renamed or deleted and the name reclaimed; the ID detects that the repo behind the name has changed.\",\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"uses\": {\n \"description\": \"The action's own direct dependencies, as canonical pin keys, so the full dependency graph stays pinned and verifiable end to end. Required for composite actions (which can call other actions); absent for leaf actions that have no dependencies of their own.\",\n \"type\": \"array\",\n \"items\": { \"$ref\": \"#/$defs/pin\" }\n }\n }\n }\n }\n}\n" -const schemaV003 = "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://gh.io/actions-lockfile/v0.0.3.json\",\n \"title\": \"GitHub Actions dependency lockfile\",\n \"description\": \"Machine-generated lockfile describing the pinned action dependency graph for a repository's workflows. Written and updated by `gh actions-lock`.\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"version\"],\n \"properties\": {\n \"version\": {\n \"description\": \"Lockfile schema version. Only v0.0.3 is supported.\",\n \"const\": \"v0.0.3\"\n },\n \"workflows\": {\n \"description\": \"Map of repo-relative workflow path to the flat, transitive list of canonical pin keys it depends on.\",\n \"type\": \"object\",\n \"additionalProperties\": {\n \"type\": \"array\",\n \"items\": { \"$ref\": \"#/$defs/pin\" }\n }\n },\n \"dependencies\": {\n \"description\": \"Deduplicated action graph keyed by canonical pin. Each entry records the resolved metadata for one action tarball.\",\n \"type\": \"object\",\n \"additionalProperties\": { \"$ref\": \"#/$defs/action\" }\n }\n },\n \"$defs\": {\n \"pin\": {\n \"description\": \"Canonical dependency pin: OWNER/REPO@REF (e.g. actions/checkout@v4).\",\n \"type\": \"string\",\n \"pattern\": \"^[^/@:]+/[^/@:]+@[^:]+$\"\n },\n \"action\": {\n \"description\": \"Resolved metadata for a single pinned action.\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"ref\", \"commit\", \"owner_id\", \"repo_id\"],\n \"properties\": {\n \"hostname\": {\n \"description\": \"The optional bare canonical hostname of the GitHub instance that owns and resolves this dependency. The value must be github.com or a lowercase GHE tenant hostname such as octocorp.ghe.com; schemes, ports, paths, query strings, fragments, and surrounding whitespace are not allowed.\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"pattern\": \"^(github\\\\.com|[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\\\.ghe\\\\.com)$\"\n },\n \"ref\": {\n \"description\": \"The git ref the commit was resolved from. Required: every dep that passes impostor checks has a resolvable ref. The CLI picks the best ref with priority: full semver tag > any tag > branch (protected > default > release/v* > any). The parser enforces presence; priority ordering is the CLI's concern.\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"commit\": {\n \"description\": \"The exact commit the action resolves to, in algo-prefixed digest form (e.g. sha1-...). This is the immutable identity the runner checks out; tags and branches can be moved, this cannot.\",\n \"type\": \"string\"\n },\n \"owner_id\": {\n \"description\": \"The numeric ID of the action's owner (user or org) on the recorded hostname. Pinned because names can be deleted and re-registered by someone else; the ID cannot, so it ties the pin to the original owner.\",\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"repo_id\": {\n \"description\": \"The numeric ID of the action's repository on the recorded hostname. Pinned because a repo can be renamed or deleted and the name reclaimed; the ID detects that the repo behind the name has changed.\",\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"uses\": {\n \"description\": \"The action's own direct dependencies, as canonical pin keys, so the full dependency graph stays pinned and verifiable end to end. Required for composite actions (which can call other actions); absent for leaf actions that have no dependencies of their own.\",\n \"type\": \"array\",\n \"items\": { \"$ref\": \"#/$defs/pin\" }\n }\n }\n }\n }\n}\n" +const schemaV003 = "{\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"$id\": \"https://gh.io/actions-lockfile/v0.0.3.json\",\n \"title\": \"GitHub Actions dependency lockfile\",\n \"description\": \"Machine-generated lockfile describing the pinned action dependency graph for a repository's workflows. Written and updated by `gh actions-lock`.\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"version\"],\n \"properties\": {\n \"version\": {\n \"description\": \"Lockfile schema version. Only v0.0.3 is supported.\",\n \"const\": \"v0.0.3\"\n },\n \"workflows\": {\n \"description\": \"Map of repo-relative workflow path to the flat, transitive list of canonical pin keys it depends on.\",\n \"type\": \"object\",\n \"additionalProperties\": {\n \"type\": \"array\",\n \"items\": { \"$ref\": \"#/$defs/pin\" }\n }\n },\n \"dependencies\": {\n \"description\": \"Deduplicated action graph keyed by canonical pin. Each entry records the resolved metadata for one action tarball.\",\n \"type\": \"object\",\n \"additionalProperties\": { \"$ref\": \"#/$defs/action\" }\n }\n },\n \"$defs\": {\n \"pin\": {\n \"description\": \"Canonical dependency pin: OWNER/REPO@REF (e.g. actions/checkout@v4).\",\n \"type\": \"string\",\n \"pattern\": \"^[^/@:]+/[^/@:]+@[^:]+$\"\n },\n \"action\": {\n \"description\": \"Resolved metadata for a single pinned action.\",\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"ref\", \"commit\", \"owner_id\", \"repo_id\"],\n \"properties\": {\n \"hostname\": {\n \"description\": \"The GitHub instance that owns and resolves this dependency. Omitted means the home host: the tenant on a GHE.com data-residency instance, or github.com on github.com. Producers omit it for home-host pins and write github.com only for github.com-bound pins on a GHE.com data-residency instance. When present, the value must be github.com or a lowercase GHE tenant hostname such as octocorp.ghe.com; schemes, ports, paths, query strings, fragments, and surrounding whitespace are not allowed.\",\n \"type\": \"string\",\n \"minLength\": 1,\n \"pattern\": \"^(github\\\\.com|[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\\\.ghe\\\\.com)$\"\n },\n \"ref\": {\n \"description\": \"The git ref the commit was resolved from. Required: every dep that passes impostor checks has a resolvable ref. The CLI picks the best ref with priority: full semver tag > any tag > branch (protected > default > release/v* > any). The parser enforces presence; priority ordering is the CLI's concern.\",\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"commit\": {\n \"description\": \"The exact commit the action resolves to, in algo-prefixed digest form (e.g. sha1-...). This is the immutable identity the runner checks out; tags and branches can be moved, this cannot.\",\n \"type\": \"string\"\n },\n \"owner_id\": {\n \"description\": \"The numeric ID of the action's owner (user or org) on the recorded hostname. Pinned because names can be deleted and re-registered by someone else; the ID cannot, so it ties the pin to the original owner.\",\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"repo_id\": {\n \"description\": \"The numeric ID of the action's repository on the recorded hostname. Pinned because a repo can be renamed or deleted and the name reclaimed; the ID detects that the repo behind the name has changed.\",\n \"type\": \"integer\",\n \"minimum\": 1\n },\n \"uses\": {\n \"description\": \"The action's own direct dependencies, as canonical pin keys, so the full dependency graph stays pinned and verifiable end to end. Required for composite actions (which can call other actions); absent for leaf actions that have no dependencies of their own.\",\n \"type\": \"array\",\n \"items\": { \"$ref\": \"#/$defs/pin\" }\n }\n }\n }\n }\n}\n" diff --git a/schema/lockfile-v0.0.3.json b/schema/lockfile-v0.0.3.json index a3158c4..fb84190 100644 --- a/schema/lockfile-v0.0.3.json +++ b/schema/lockfile-v0.0.3.json @@ -38,7 +38,7 @@ "required": ["ref", "commit", "owner_id", "repo_id"], "properties": { "hostname": { - "description": "The optional bare canonical hostname of the GitHub instance that owns and resolves this dependency. The value must be github.com or a lowercase GHE tenant hostname such as octocorp.ghe.com; schemes, ports, paths, query strings, fragments, and surrounding whitespace are not allowed.", + "description": "The GitHub instance that owns and resolves this dependency. Omitted means the home host: the tenant on a GHE.com data-residency instance, or github.com on github.com. Producers omit it for home-host pins and write github.com only for github.com-bound pins on a GHE.com data-residency instance. When present, the value must be github.com or a lowercase GHE tenant hostname such as octocorp.ghe.com; schemes, ports, paths, query strings, fragments, and surrounding whitespace are not allowed.", "type": "string", "minLength": 1, "pattern": "^(github\\.com|[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.ghe\\.com)$"