{"id":30602145,"url":"https://github.com/grantbirki/gh-pin","last_synced_at":"2026-05-17T11:31:56.324Z","repository":{"id":309622028,"uuid":"1036962509","full_name":"GrantBirki/gh-pin","owner":"GrantBirki","description":"Pin Docker container images to an exact index digest for better build reproducibility","archived":false,"fork":false,"pushed_at":"2025-08-12T22:41:45.000Z","size":40766,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-12T23:33:35.873Z","etag":null,"topics":["cli","containers","docker","gh-cli","reproducibility","security"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/GrantBirki.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-12T21:08:20.000Z","updated_at":"2025-08-12T22:41:48.000Z","dependencies_parsed_at":"2025-08-12T23:43:48.495Z","dependency_job_id":null,"html_url":"https://github.com/GrantBirki/gh-pin","commit_stats":null,"previous_names":["grantbirki/gh-pin"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/GrantBirki/gh-pin","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrantBirki%2Fgh-pin","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrantBirki%2Fgh-pin/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrantBirki%2Fgh-pin/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrantBirki%2Fgh-pin/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/GrantBirki","download_url":"https://codeload.github.com/GrantBirki/gh-pin/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GrantBirki%2Fgh-pin/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":272784494,"owners_count":24992465,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-29T02:00:10.610Z","response_time":87,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","containers","docker","gh-cli","reproducibility","security"],"created_at":"2025-08-30T00:14:44.258Z","updated_at":"2026-05-17T11:31:56.318Z","avatar_url":"https://github.com/GrantBirki.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# gh-pin 📌\n\n[![test](https://github.com/grantbirki/gh-pin/actions/workflows/test.yml/badge.svg)](https://github.com/grantbirki/gh-pin/actions/workflows/test.yml)\n[![build](https://github.com/grantbirki/gh-pin/actions/workflows/build.yml/badge.svg)](https://github.com/grantbirki/gh-pin/actions/workflows/build.yml)\n[![lint](https://github.com/grantbirki/gh-pin/actions/workflows/lint.yml/badge.svg)](https://github.com/grantbirki/gh-pin/actions/workflows/lint.yml)\n[![acceptance](https://github.com/grantbirki/gh-pin/actions/workflows/acceptance.yml/badge.svg)](https://github.com/grantbirki/gh-pin/actions/workflows/acceptance.yml)\n[![release](https://github.com/grantbirki/gh-pin/actions/workflows/release.yml/badge.svg)](https://github.com/grantbirki/gh-pin/actions/workflows/release.yml)\n![slsa-level3](docs/assets/slsa-level3.svg)\n\nPin Docker container images and GitHub Actions to exact digests for better build reproducibility.\n\n![gh-pin](docs/assets/gh-pin.png)\n\n## About ⭐\n\nThis project is a [`gh cli`](https://github.com/cli/cli) extension that is used to pin Docker container images and GitHub Actions to exact digests. This is useful for ensuring that builds are reproducible and secure.\n\nContainer images referenced by mutable tags (like `latest` or `v1.0`) and GitHub Actions referenced by mutable tags (like `v4` or `main`) can change over time, leading to inconsistent builds and potential security vulnerabilities. When a tag is updated to point to a new version, your builds may suddenly start using different dependencies or even malicious content without your knowledge.\n\nThe `gh pin` tool solves this by automatically converting mutable references to immutable digest references. Instead of `ubuntu:latest`, your files will reference `ubuntu@sha256:abc123...`, and instead of `actions/checkout@v5`, your workflows will reference `actions/checkout@sha123abc`. This ensures that the exact same versions are used every time. This approach follows security best practices recommended by organizations like the [CNCF](https://www.cncf.io/online-programs/cloud-native-live-automate-pinning-github-actions-and-container-images-to-their-digests/) and [SLSA](https://slsa.dev/) for supply chain security.\n\nAll updated pins (Dependabot + Actions) will work out of the box with Dependabot!\n\n\u003e Moving towards immutable image references lives in the same ecosystem as [Hermetic Builds](https://software.birki.io/posts/hermetic-builds/) which is a topic I am passionate about and a key reason for building this CLI.\n\n## Installation 💻\n\nInstall this gh cli extension by running the following command:\n\n```bash\ngh extension install grantbirki/gh-pin\n```\n\n### Upgrading 📦\n\nYou can upgrade this extension by running the following command:\n\n```bash\ngh ext upgrade pin\n```\n\n## Usage 🚀\n\n### Basic Usage\n\nPin images in a specific Dockerfile:\n\n```bash\ngh pin Dockerfile\n```\n\nPin images in a specific Dockerfile using an exact platform:\n\n```bash\ngh pin --platform=linux/amd64 Dockerfile\n```\n\nPin images in a specific Docker compose file:\n\n```bash\ngh pin docker-compose.yml\n```\n\nPin GitHub Actions in a workflow file:\n\n```bash\ngh pin .github/workflows/ci.yml\n```\n\nPin all Docker images and GitHub Actions in the current directory and subdirectories:\n\n```bash\ngh pin .\n```\n\n\u003e Note: The `gh pin` command works best when you run it from the root of your repository when using `gh pin .`\n\nPin all images and actions in a specific directory and its subdirectories:\n\n```bash\ngh pin /path/to/project\n```\n\n### Command Line Options\n\n| Flag | Description | Default |\n|------|-------------|---------|\n| `--algo` | Digest algorithm to check for (sha256, sha512, etc.) | `sha256` |\n| `--dry-run` | Preview changes without writing files | `false` |\n| `--expand-registry` | Expand short image names to fully qualified registry names | `false` |\n| `--no-color` | Disable colored output | `false` |\n| `--pervasive` | Scan all YAML files, not just docker-compose files | `false` |\n| `--platform` | Target specific platform architecture (e.g., `linux/amd64`, `linux/arm64`) | (uses index digest) |\n| `--recursive` | Scan directories recursively | `true` |\n| `--version` | Show version information | `false` |\n\n### Examples\n\n**Preview changes without modifying files:**\n\n```bash\ngh pin --dry-run\n```\n\n**Pin images and expand registry names:**\n\n```bash\n# Without --expand-registry (default):\n# ubuntu:latest → ubuntu:latest@sha256:abc123...\n\n# With --expand-registry:\n# ubuntu:latest → docker.io/library/ubuntu:latest@sha256:abc123...\ngh pin --expand-registry\n```\n\n**Scan all YAML files (including Kubernetes manifests, CI configs, etc.):**\n\n```bash\ngh pin --pervasive\n```\n\n**Combine multiple options:**\n\n```bash\ngh pin --dry-run --pervasive --expand-registry /path/to/project\n```\n\n**Pin to specific platform architecture:**\n\n```bash\n# Pin for linux/amd64 architecture specifically\ngh pin --platform=linux/amd64 docker-compose.yml\n\n# Pin for ARM64 (Apple Silicon, AWS Graviton instances)\ngh pin --platform=linux/arm64 .\n```\n\n### Supported File Types\n\n| File Type | Detection Pattern | Description |\n|-----------|------------------|-------------|\n| **Dockerfiles** | `Dockerfile*` | Any file starting with \"Dockerfile\" (ex: `Dockerfile`, `Dockerfile.test`, `Dockerfile.dev`, etc) |\n| **Docker Compose** | `docker-compose.yml`, `docker-compose.yaml` | Standard Docker Compose files |\n| **GitHub Actions** | `.github/workflows/*.yml`, `.github/workflows/*.yaml` | GitHub Actions workflow files |\n| **Generic YAML** | `*.yml`, `*.yaml` | When using `--pervasive` flag |\n\n### Platform-Specific Pinning\n\nThe `--platform` flag allows you to pin images to platform-specific manifest digests instead of multi-platform index digests.\n\n**Index Digests (Default Behavior):**\n\n```bash\n# Pins to the multi-platform index digest\ngh pin Dockerfile\n# Result: FROM nginx:latest@sha256:abc123...\n```\n\n**Platform-Specific Manifest Digests:**\n\nIncreased determinism by pinning to a specific platform's manifest digest:\n\n```bash\n# Pins to the linux/amd64 specific manifest digest\ngh pin --platform=linux/amd64 Dockerfile\n# Result: FROM nginx:latest@sha256:def456...\n```\n\n#### When to Use Index vs Platform-Specific Digests\n\n**Use Index Digests (Default) when:**\n\n- You want maximum compatibility across different architectures\n- Your build system automatically selects the correct platform\n- You're building multi-platform images that should work everywhere\n- You want to maintain flexibility for different deployment environments\n\n**Use Platform-Specific Digests (`--platform`) when:**\n\n- You need deterministic builds for a specific architecture\n- You're building for embedded systems or specific hardware\n- You want to ensure the exact same binary artifacts every time\n- You're troubleshooting platform-specific issues\n- Your deployment targets only run on specific architectures\n\n#### Supported Platforms\n\nCommon platform specifications include:\n\n- `linux/amd64` - 64-bit x86 Linux (most common)\n- `linux/arm64` - 64-bit ARM Linux (Apple Silicon, AWS Graviton, etc.)\n- `linux/arm/v7` - 32-bit ARM Linux\n- `linux/arm64/v8` - 64-bit ARM Linux (Raspberry Pi, etc.)\n- `windows/amd64` - 64-bit x86 Windows\n- `darwin/amd64` - 64-bit x86 macOS\n- `darwin/arm64` - 64-bit ARM macOS (Apple Silicon)\n\n#### Platform Examples\n\n**Target specific architecture:**\n\n```bash\ngh pin --platform=linux/amd64 docker-compose.yml\n\ngh pin --platform=linux/arm64 Dockerfile\n\ngh pin --platform=linux/arm/v7 docker-compose.yml\n```\n\n**Error handling:**\n\n```bash\n# If the provided platform doesn't exist it will gracefully falls back to index digest\ngh pin --platform=invalid/platform Dockerfile\n# Warning: Could not find manifest for platform invalid/platform. Falling back to index digest.\n```\n\n#### Index vs Platform Digest Comparison\n\n| Aspect | Index Digest (Default) | Platform-Specific Digest |\n|--------|------------------------|--------------------------|\n| **Compatibility** | Works across all supported platforms | Works only on specified platform |\n| **Build Reproducibility** | Good (platform selected at runtime) | Excellent (exact binary artifacts) |\n| **Output Format** | `image:tag@sha256:hash` | `image:tag@sha256:hash` |\n| **Use Case** | General development, CI/CD | Specific deployments, debugging, hardened CI/CD environment |\n| **Fallback** | N/A | Falls back to index if platform unavailable |\n\n### Force Mode\n\nYou can force the tool to only process specific file types:\n\n```bash\n# Only process Docker-related files\ngh pin --mode=docker\n\n# Only process GitHub Actions workflows\ngh pin --mode=actions\n```\n\n### Output Examples\n\n```bash\n$ gh pin --dry-run\n📌 [DOCKERFILE] ubuntu:latest → ubuntu:latest@sha256:7c06e91f61fa88c08cc74f7e1b7c69ae24910d745357e0dfe1d2c0322aaf20f9\n📌 [COMPOSE] nginx:alpine → nginx:alpine@sha256:2d194b9da5f3b2f19d8b03b48d36c3f8af53e24b96b8c48a82db8d7b6e6e4c6a\n📌 [ACTIONS] actions/checkout@v5 → actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5\n```\n\n```bash\n$ gh pin --platform=linux/amd64 --dry-run\n📌 [DOCKERFILE] ubuntu:latest → ubuntu:latest@sha256:1b8d8ff4777f36f19bfe73ee4df61e3a0b789caeff29caa019539ec7c9a57f95\n📌 [COMPOSE] nginx:alpine → nginx:alpine@sha256:a97eb9ecc708c8aa715ddc4b375e7c130bd32e0bce17c74b4f8c3a90e8338e14\n📌 [ACTIONS] actions/checkout@v5 → actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # pin@v5\n```\n\n#### What Gets Written to Files\n\n**Both Index and Platform-Specific Digests:** Files are updated with the following format:\n\n```dockerfile\n# Dockerfile - BEFORE\nFROM ubuntu:latest\n\n# Dockerfile - AFTER  \nFROM ubuntu:latest@sha256:7c06e91f61fa88c08cc74f7e1b7c69ae24910d745357e0dfe1d2c0322aaf20f9\n```\n\n```yaml\n# docker-compose.yml - BEFORE\nservices:\n  web:\n    image: nginx:alpine\n\n# docker-compose.yml - AFTER\nservices:\n  web:\n    image: nginx:alpine@sha256:2d194b9da5f3b2f19d8b03b48d36c3f8af53e24b96b8c48a82db8d7b6e6e4c6a\n```\n\n### Exit Codes\n\n- `0`: Success - all resources processed successfully\n- `1`: Error - failed to process one or more resources\n\n## How it Works 📚\n\nThe `gh-pin` CLI scans your project files and replaces mutable references with immutable digest references for better security and reproducibility.\n\n### High-Level Process\n\n1. **File Discovery**: Recursively scans directories to find supported files:\n   - `Dockerfile*` (any file starting with \"Dockerfile\")\n   - `docker-compose.yml/yaml` files\n   - `.github/workflows/*.yml` GitHub Actions workflow files\n   - Generic `.yml/.yaml` files (when using `--pervasive` flag)\n\n2. **Reference Detection**: Parses files to identify mutable references:\n   - Extracts `FROM` statements in Dockerfiles\n   - Finds `image:` fields in Compose/YAML files\n   - Extracts `uses:` statements in GitHub Actions workflows\n   - Skips references that already have digest/SHA pinning\n\n3. **Resolution**: For each unpinned reference, performs API queries:\n   - **Container Images**: Makes HTTP `HEAD` requests to container registries (Docker Hub, GHCR, etc.)\n   - **GitHub Actions**: Makes API requests to GitHub to resolve tags to commit SHAs\n   - Retrieves digest/SHA that uniquely identifies the version\n\n4. **File Updates**: Replaces mutable references with immutable ones while preserving file structure:\n   - **Clean format**: Updates with tag@digest format (e.g., `nginx:alpine@sha256:abc123`) for all images\n   - **Docker compatible**: Uses valid Docker syntax that works in all contexts\n   - **Human readable**: Preserves original tag information in the reference\n   - **Existing comments**: Preserved unchanged when already present\n   - **File formatting**: Original indentation, order, and structure maintained\n\n### Understanding Index vs Manifest Digests\n\nWhen pinning container images, `gh-pin` can target two different types of digests:\n\n**Index Digests (Default):**\n\n- Point to a **manifest list/index** that contains multiple platform-specific manifests\n- Allow Docker/container runtimes to automatically select the correct platform at pull time\n- Provide maximum compatibility across different architectures\n- Example: `nginx@sha256:abc123...` works on AMD64, ARM64, etc.\n\n**Platform-Specific Manifest Digests (`--platform` flag):**\n\n- Point directly to a **single platform's manifest**\n- Ensure you get exactly the same binary artifacts every time\n- Provide deterministic builds for specific architectures\n- Example: `nginx:latest@sha256:def456...` only works on the specified platform\n\n**Visual Example:**\n\n```text\nContainer Registry:\n├── nginx:latest (index/manifest list)\n│   ├── linux/amd64 → manifest digest: sha256:aaa111...\n│   ├── linux/arm64 → manifest digest: sha256:bbb222...\n│   └── linux/arm/v7 → manifest digest: sha256:ccc333...\n└── Index digest: sha256:index123...\n\nDefault: nginx@sha256:index123... (points to manifest list)\n--platform=linux/amd64: nginx:latest@sha256:aaa111... (points to specific manifest)\n```\n\n1. **File Updates**: Replaces mutable references with immutable digest references:\n   - `nginx:latest` → `nginx@sha256:abc123...`\n   - `ubuntu:20.04` → `ubuntu@sha256:def456...`\n   - `actions/checkout@v5` → `actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8`\n   - Preserves original formatting and indentation\n   - Supports comment-based pinning (e.g., `# pin@v5`)\n\n### Benefits\n\n- **Reproducible Builds**: Same digest/SHA always references the exact same version\n- **Security**: Prevents supply chain attacks from tag manipulation\n- **Efficiency**: Uses HEAD requests to minimize network bandwidth\n- **Compatibility**: Works with all OCI-compatible registries and GitHub Actions\n- **Comment Support**: Supports `# pin@v5` style comments for explicit version control\n\n## Prior Art, Inspiration, and Alternatives\n\n- [mheap/pin-github-action](https://github.com/mheap/pin-github-action)\n- Follow a guide like this from [Step Security](https://www.stepsecurity.io/blog/pinning-github-actions-for-enhanced-security-a-complete-guide) and manually update Actions pins then use dependabot\n\nYou can pull Docker digests manually by pulling down the entire image (can be slow) like this:\n\n```bash\nTAG=\"postgres:15\"\ndocker pull \"$TAG\"\nDIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \"$TAG\")\necho \"$TAG -\u003e $DIGEST\"\n```\n\nYou could also do something like this and manually edit your Docker / Docker-Compose files:\n\n```bash\nregctl image digest postgres:15\n# outputs: sha256:9b2a...\n```\n\n## Verifying Release Binaries 🔏\n\nThis project uses [goreleaser](https://goreleaser.com/) to build binaries and [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) to publish the provenance of the release.\n\nYou can verify the release binaries by following these steps:\n\n1. Download a release from the [releases page](https://github.com/grantbirki/gh-pin/releases).\n2. Verify it `gh attestation verify --owner github ~/Downloads/darwin-arm64` (an example for darwin-arm64).\n\n---\n\nRun `gh pin --help` for more information and full command/options usage.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrantbirki%2Fgh-pin","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgrantbirki%2Fgh-pin","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrantbirki%2Fgh-pin/lists"}