├── src ├── __fixtures__ │ ├── workflow │ │ ├── readme.md │ │ ├── script.sh │ │ ├── workflow-invalid.yml │ │ ├── workflow-ci.yml │ │ ├── workflow-top.yml │ │ ├── workflow-nested.yml │ │ ├── workflow-deploy.yml │ │ ├── workflow-mixed-dir.yml │ │ ├── workflow-valid.yml │ │ ├── workflow-yaml-extension.yaml │ │ └── workflow-multi-job.yml │ ├── list │ │ ├── lockfile-empty.json │ │ ├── lockfile-sha-display.json │ │ ├── lockfile-multi-version.json │ │ ├── lockfile-basic-tree.json │ │ ├── lockfile-tree-formatting.json │ │ └── lockfile-composite-deps.json │ ├── action-node.yml │ ├── index │ │ ├── workflow-verify-test.yml │ │ └── lockfile-cli-test.json │ ├── generate │ │ ├── workflow-ci.yml │ │ ├── workflow-no-actions.yml │ │ ├── workflow-single-action.yml │ │ ├── workflow-deploy.yml │ │ └── workflow-multi-version.yml │ ├── verify │ │ ├── workflow-match.yml │ │ ├── workflow-mismatch-v5.yml │ │ ├── workflow-removed-actions.yml │ │ ├── workflow-new-actions.yml │ │ ├── lockfile-new-actions.json │ │ ├── lockfile-match.json │ │ ├── lockfile-mismatch-v4.json │ │ └── lockfile-removed-actions.json │ ├── workflow-no-actions.yml │ ├── workflow-basic.yml │ ├── action-composite.yml │ ├── workflow-special-refs.yml │ ├── workflow-multiple-jobs.yml │ ├── lockfile-with-deps.json │ ├── lockfile-sample.json │ └── helpers.ts ├── commands │ ├── verify.ts │ ├── generate.ts │ ├── list.ts │ ├── verify.test.ts │ ├── list.test.ts │ └── generate.test.ts ├── utils │ └── directory.ts ├── types.ts ├── index.ts ├── parser │ ├── workflow.ts │ └── workflow.test.ts ├── resolver │ └── resolver.ts ├── lockfile │ ├── lockfile.ts │ └── lockfile.test.ts ├── github │ ├── client.ts │ └── client.test.ts └── index.test.ts ├── .release-please-manifest.json ├── .github ├── FUNDING.yml ├── workflows │ ├── test.yml │ ├── lint.yml │ ├── automerge.yml │ ├── release-please.yml │ ├── publish.yml │ ├── deploy-website.yml │ └── actions.lock.json └── dependabot.yml ├── website ├── src │ ├── pages │ │ ├── docs │ │ │ ├── index.astro │ │ │ └── [...slug].astro │ │ └── index.astro │ ├── lib │ │ ├── utils.ts │ │ └── docs-config.ts │ ├── content │ │ ├── config.ts │ │ └── docs │ │ │ ├── commands.md │ │ │ ├── cli-reference.mdx │ │ │ ├── getting-started.md │ │ │ └── usage.mdx │ ├── components │ │ ├── Footer.astro │ │ ├── docs │ │ │ ├── GitHubTokenNote.astro │ │ │ ├── DocsSidebar.astro │ │ │ ├── DocsLayout.astro │ │ │ ├── MobileNav.tsx │ │ │ └── Callout.astro │ │ ├── ThemeToggle.tsx │ │ ├── Header.astro │ │ ├── BaseLayout.astro │ │ └── ui │ │ │ ├── button.tsx │ │ │ └── card.tsx │ └── styles │ │ └── globals.css ├── public │ ├── favicon.ico │ └── icon.svg ├── tsconfig.json ├── astro.config.mjs └── package.json ├── release-please-config.json ├── .vscode └── settings.json ├── tsconfig.json ├── CHANGELOG.md ├── release.md ├── package.json ├── action.yml ├── .gitignore └── README.md /src/__fixtures__/workflow/readme.md: -------------------------------------------------------------------------------- 1 | # README 2 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow/script.sh: -------------------------------------------------------------------------------- 1 | echo hello 2 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "1.0.0" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: gjtorikian 4 | -------------------------------------------------------------------------------- /website/src/pages/docs/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | return Astro.redirect("/docs/getting-started"); 3 | --- 4 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow/workflow-invalid.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | bad indentation here 3 | another: line 4 | -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gjtorikian/gh-actions-lockfile/HEAD/website/public/favicon.ico -------------------------------------------------------------------------------- /src/__fixtures__/list/lockfile-empty.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": {} 5 | } 6 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow/workflow-ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo ci 8 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow/workflow-top.yml: -------------------------------------------------------------------------------- 1 | name: Top 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo test 8 | -------------------------------------------------------------------------------- /src/__fixtures__/action-node.yml: -------------------------------------------------------------------------------- 1 | name: Node Action 2 | description: A JavaScript action (no transitive deps) 3 | 4 | runs: 5 | using: node20 6 | main: dist/index.js 7 | -------------------------------------------------------------------------------- /src/__fixtures__/index/workflow-verify-test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo test 8 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow/workflow-nested.yml: -------------------------------------------------------------------------------- 1 | name: Nested 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo test 8 | -------------------------------------------------------------------------------- /src/__fixtures__/generate/workflow-ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | -------------------------------------------------------------------------------- /src/__fixtures__/verify/workflow-match.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow/workflow-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: push 3 | jobs: 4 | deploy: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo deploy 8 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow/workflow-mixed-dir.yml: -------------------------------------------------------------------------------- 1 | name: Workflow 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo test 8 | -------------------------------------------------------------------------------- /src/__fixtures__/generate/workflow-no-actions.yml: -------------------------------------------------------------------------------- 1 | name: Script Only 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo "Hello" 8 | -------------------------------------------------------------------------------- /src/__fixtures__/verify/workflow-mismatch-v5.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v5 8 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow/workflow-valid.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | -------------------------------------------------------------------------------- /src/__fixtures__/generate/workflow-single-action.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | -------------------------------------------------------------------------------- /src/__fixtures__/verify/workflow-removed-actions.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow/workflow-yaml-extension.yaml: -------------------------------------------------------------------------------- 1 | name: YAML Extension 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo test 8 | -------------------------------------------------------------------------------- /website/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /src/__fixtures__/generate/workflow-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | on: push 3 | jobs: 4 | deploy: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - uses: actions/setup-node@v4 9 | -------------------------------------------------------------------------------- /src/__fixtures__/verify/workflow-new-actions.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v4 8 | - uses: actions/setup-node@v4 9 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | test: 8 | uses: gjtorikian/actions/.github/workflows/bun_test.yml@main 9 | secrets: 10 | gh_token: ${{ secrets.GITHUB_TOKEN }} 11 | 12 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow/workflow-multi-job.yml: -------------------------------------------------------------------------------- 1 | name: Multi 2 | on: push 3 | jobs: 4 | lint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - run: echo lint 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - run: echo test 12 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "jsxImportSource": "react", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | 6 | env: 7 | NODE_ENV: test 8 | 9 | jobs: 10 | lint: 11 | uses: gjtorikian/actions/.github/workflows/bun_lint.yml@main 12 | secrets: 13 | gh_token: ${{ secrets.GITHUB_TOKEN }} 14 | 15 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow-no-actions.yml: -------------------------------------------------------------------------------- 1 | name: No Actions 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | script: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - run: echo "Hello, World!" 9 | - run: | 10 | echo "Multi-line script" 11 | date 12 | -------------------------------------------------------------------------------- /src/__fixtures__/generate/workflow-multi-version.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | build-v3: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v3 8 | build-v4: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", 3 | "packages": { 4 | ".": { 5 | "release-type": "node", 6 | "changelog-path": "CHANGELOG.md", 7 | "include-component-in-tag": false 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: PR auto-{approve,merge} 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request_target: 6 | 7 | permissions: 8 | pull-requests: write 9 | contents: write 10 | 11 | jobs: 12 | bot-check: 13 | uses: gjtorikian/actions/.github/workflows/automerge.yml@main 14 | secrets: inherit 15 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow-basic.yml: -------------------------------------------------------------------------------- 1 | name: Basic Workflow 2 | on: push 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: actions/setup-node@v4 10 | with: 11 | node-version: "20" 12 | - run: npm install 13 | - run: npm test 14 | -------------------------------------------------------------------------------- /src/__fixtures__/verify/lockfile-new-actions.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": { 5 | "actions/checkout": [ 6 | { 7 | "version": "v4", 8 | "sha": "abc123", 9 | "integrity": "sha256-xyz", 10 | "dependencies": [] 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/__fixtures__/verify/lockfile-match.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": { 5 | "actions/checkout": [ 6 | { 7 | "version": "v4", 8 | "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", 9 | "integrity": "sha256-abc123", 10 | "dependencies": [] 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /website/src/content/config.ts: -------------------------------------------------------------------------------- 1 | import { defineCollection, z } from "astro:content"; 2 | 3 | const docsCollection = defineCollection({ 4 | type: "content", 5 | schema: z.object({ 6 | title: z.string(), 7 | description: z.string(), 8 | order: z.number().optional(), 9 | }), 10 | }); 11 | 12 | export const collections = { 13 | docs: docsCollection, 14 | }; 15 | -------------------------------------------------------------------------------- /src/__fixtures__/action-composite.yml: -------------------------------------------------------------------------------- 1 | name: Composite Action 2 | description: A composite action with dependencies 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - uses: actions/checkout@v4 8 | with: 9 | fetch-depth: 0 10 | - uses: actions/setup-node@v4 11 | with: 12 | node-version: "20" 13 | - run: npm install 14 | shell: bash 15 | -------------------------------------------------------------------------------- /src/__fixtures__/index/lockfile-cli-test.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": { 5 | "actions/checkout": [ 6 | { 7 | "version": "v4", 8 | "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", 9 | "integrity": "sha256-abc123", 10 | "dependencies": [] 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/__fixtures__/list/lockfile-sha-display.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": { 5 | "actions/checkout": [ 6 | { 7 | "version": "v4", 8 | "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", 9 | "integrity": "sha256-abc123", 10 | "dependencies": [] 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/__fixtures__/verify/lockfile-mismatch-v4.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": { 5 | "actions/checkout": [ 6 | { 7 | "version": "v4", 8 | "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", 9 | "integrity": "sha256-abc123", 10 | "dependencies": [] 11 | } 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /website/src/components/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | interface Props { 3 | githubUrl: string; 4 | } 5 | 6 | const { githubUrl } = Astro.props; 7 | --- 8 | 9 | 14 | -------------------------------------------------------------------------------- /website/src/components/docs/GitHubTokenNote.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Callout from "./Callout.astro"; 3 | --- 4 | 5 | 6 |

7 | When running locally, set a GITHUB_TOKEN environment variable to 8 | avoid rate limits. Without it, you're limited to 60 API requests per hour. 9 |

10 |
export GITHUB_TOKEN=ghp_your_token_here
11 |
12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | 8 | - package-ecosystem: npm 9 | directory: "/" 10 | schedule: 11 | interval: monthly 12 | day: monday 13 | time: "09:00" 14 | timezone: "Etc/UTC" 15 | open-pull-requests-limit: 10 16 | groups: 17 | npm-dependencies: 18 | patterns: 19 | - "*" 20 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow-special-refs.yml: -------------------------------------------------------------------------------- 1 | name: Special Refs 2 | on: push 3 | 4 | jobs: 5 | test: 6 | runs-on: ubuntu-latest 7 | steps: 8 | # Local action (should be skipped) 9 | - uses: ./local-action 10 | 11 | # Docker action (should be skipped) 12 | - uses: docker://alpine:3.18 13 | 14 | # Action with path 15 | - uses: actions/cache/restore@v4 16 | 17 | # Action with SHA 18 | - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 19 | -------------------------------------------------------------------------------- /src/__fixtures__/verify/lockfile-removed-actions.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": { 5 | "actions/checkout": [ 6 | { 7 | "version": "v4", 8 | "sha": "abc123", 9 | "integrity": "sha256-xyz", 10 | "dependencies": [] 11 | } 12 | ], 13 | "actions/setup-node": [ 14 | { 15 | "version": "v4", 16 | "sha": "def456", 17 | "integrity": "sha256-abc", 18 | "dependencies": [] 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/__fixtures__/list/lockfile-multi-version.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": { 5 | "actions/checkout": [ 6 | { 7 | "version": "v3", 8 | "sha": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 9 | "integrity": "sha256-v3hash", 10 | "dependencies": [] 11 | }, 12 | { 13 | "version": "v4", 14 | "sha": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", 15 | "integrity": "sha256-v4hash", 16 | "dependencies": [] 17 | } 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Release Please 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | jobs: 13 | release-please: 14 | runs-on: ubuntu-latest 15 | outputs: 16 | releases_created: ${{ steps.release.outputs.releases_created }} 17 | paths_released: ${{ steps.release.outputs.paths_released }} 18 | steps: 19 | - uses: googleapis/release-please-action@v4 20 | id: release 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /src/__fixtures__/list/lockfile-basic-tree.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": { 5 | "actions/checkout": [ 6 | { 7 | "version": "v4", 8 | "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", 9 | "integrity": "sha256-abc123", 10 | "dependencies": [] 11 | } 12 | ], 13 | "actions/setup-node": [ 14 | { 15 | "version": "v4", 16 | "sha": "60edb5dd545a775178f52524783378180af0d1f8", 17 | "integrity": "sha256-xyz789", 18 | "dependencies": [] 19 | } 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /website/src/pages/docs/[...slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getCollection } from "astro:content"; 3 | import DocsLayout from "@/components/docs/DocsLayout.astro"; 4 | 5 | export async function getStaticPaths() { 6 | const docs = await getCollection("docs"); 7 | return docs.map((doc) => ({ 8 | params: { slug: doc.slug }, 9 | props: { doc }, 10 | })); 11 | } 12 | 13 | const { doc } = Astro.props; 14 | const { Content } = await doc.render(); 15 | --- 16 | 17 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /website/astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import react from '@astrojs/react'; 3 | import mdx from '@astrojs/mdx'; 4 | import tailwindcss from '@tailwindcss/vite'; 5 | 6 | export default defineConfig({ 7 | vite: { 8 | plugins: [tailwindcss()], 9 | }, 10 | image: { 11 | service: { 12 | entrypoint: 'astro/assets/services/noop', 13 | }, 14 | }, 15 | markdown: { 16 | shikiConfig: { 17 | themes: { 18 | light: 'solarized-light', 19 | dark: 'solarized-dark', 20 | }, 21 | defaultColor: false, 22 | }, 23 | }, 24 | integrations: [react(), mdx()], 25 | }); 26 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[markdown]": { 3 | "editor.defaultFormatter": "esbenp.prettier-vscode" 4 | }, 5 | "[jsonc]": { 6 | "editor.defaultFormatter": "vscode.json-language-features" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "vscode.json-language-features" 10 | }, 11 | "[github-actions-workflow]": { 12 | "editor.defaultFormatter": "redhat.vscode-yaml" 13 | }, 14 | "[typescript]": { 15 | "editor.defaultFormatter": "vscode.typescript-language-features" 16 | }, 17 | "[astro]": { 18 | "editor.defaultFormatter": "astro-build.astro-vscode" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /website/src/lib/docs-config.ts: -------------------------------------------------------------------------------- 1 | export interface NavItem { 2 | title: string; 3 | href: string; 4 | } 5 | 6 | export interface NavSection { 7 | title: string; 8 | items: NavItem[]; 9 | } 10 | 11 | export const docsNavigation: NavSection[] = [ 12 | { 13 | title: "Guide", 14 | items: [ 15 | { title: "Getting Started", href: "/docs/getting-started" }, 16 | { title: "Usage", href: "/docs/usage" }, 17 | { title: "Commands", href: "/docs/commands" }, 18 | { title: "CLI Reference", href: "/docs/cli-reference" }, 19 | ], 20 | }, 21 | ]; 22 | 23 | export const githubUrl = "https://github.com/gjtorikian/gh-actions-lockfile"; 24 | -------------------------------------------------------------------------------- /website/public/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/src/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | import { Moon, Sun } from "lucide-react"; 2 | import { Button } from "@/components/ui/button"; 3 | 4 | export function ThemeToggle() { 5 | return ( 6 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/__fixtures__/workflow-multiple-jobs.yml: -------------------------------------------------------------------------------- 1 | name: Multiple Jobs 2 | on: pull_request 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: actions/setup-node@v4 10 | - run: npm run lint 11 | 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | - run: npm test 18 | 19 | deploy: 20 | runs-on: ubuntu-latest 21 | needs: [lint, test] 22 | steps: 23 | - uses: actions/checkout@v4 24 | - uses: aws-actions/configure-aws-credentials@v4 25 | with: 26 | role-to-assume: arn:aws:iam::123456789:role/deploy 27 | - run: ./deploy.sh 28 | -------------------------------------------------------------------------------- /src/__fixtures__/lockfile-with-deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": { 5 | "owner/composite-action": { 6 | "version": "v1", 7 | "sha": "1111111111111111111111111111111111111111", 8 | "integrity": "sha256-composite123", 9 | "dependencies": [ 10 | { 11 | "ref": "actions/checkout@v4", 12 | "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", 13 | "integrity": "sha256-abc123def456" 14 | } 15 | ] 16 | }, 17 | "actions/checkout": { 18 | "version": "v4", 19 | "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", 20 | "integrity": "sha256-abc123def456", 21 | "dependencies": [] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | "resolveJsonModule": true, 13 | "noUncheckedIndexedAccess": true, 14 | "isolatedModules": true, 15 | "verbatimModuleSyntax": true, 16 | "declaration": true, 17 | "declarationDir": "./dist/types", 18 | "emitDeclarationOnly": true, 19 | "types": [ 20 | "bun" 21 | ] 22 | }, 23 | "include": [ 24 | "src/**/*" 25 | ], 26 | "exclude": [ 27 | "node_modules", 28 | "dist" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/__fixtures__/lockfile-sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": { 5 | "actions/checkout": { 6 | "version": "v4", 7 | "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", 8 | "integrity": "sha256-abc123def456", 9 | "dependencies": [] 10 | }, 11 | "actions/setup-node": { 12 | "version": "v4", 13 | "sha": "60edb5dd545a775178f52524783378180af0d1f8", 14 | "integrity": "sha256-xyz789ghi012", 15 | "dependencies": [] 16 | }, 17 | "aws-actions/configure-aws-credentials": { 18 | "version": "v4", 19 | "sha": "010d0104bdb0b0e87f8ca1779b10f08ed8f5a3ac", 20 | "integrity": "sha256-jkl345mno678", 21 | "dependencies": [] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/__fixtures__/list/lockfile-tree-formatting.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": { 5 | "owner/composite": [ 6 | { 7 | "version": "v1", 8 | "sha": "1111111111111111111111111111111111111111", 9 | "integrity": "sha256-comp", 10 | "dependencies": [ 11 | { 12 | "ref": "actions/checkout@v4", 13 | "sha": "abc123", 14 | "integrity": "sha256-abc" 15 | } 16 | ] 17 | } 18 | ], 19 | "actions/checkout": [ 20 | { 21 | "version": "v4", 22 | "sha": "abc123def456abc123def456abc123def456abc1", 23 | "integrity": "sha256-abc", 24 | "dependencies": [] 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/__fixtures__/list/lockfile-composite-deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "generated": "2024-01-15T10:30:00.000Z", 4 | "actions": { 5 | "owner/composite-action": [ 6 | { 7 | "version": "v1", 8 | "sha": "1111111111111111111111111111111111111111", 9 | "integrity": "sha256-composite", 10 | "dependencies": [ 11 | { 12 | "ref": "actions/checkout@v4", 13 | "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", 14 | "integrity": "sha256-abc123" 15 | } 16 | ] 17 | } 18 | ], 19 | "actions/checkout": [ 20 | { 21 | "version": "v4", 22 | "sha": "b4ffde65f46336ab88eb53be808477a3936bae11", 23 | "integrity": "sha256-abc123", 24 | "dependencies": [] 25 | } 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | id-token: write 10 | contents: read 11 | 12 | jobs: 13 | publish: 14 | if: > 15 | github.event_name == 'workflow_dispatch' || 16 | (github.event.pull_request.merged == true && 17 | startsWith(github.head_ref, 'release-please')) 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v6 21 | 22 | - name: Set up Bun 23 | uses: gjtorikian/actions/setup-languages/bun@main 24 | 25 | - name: Set up Node (for npm publish with OIDC) 26 | uses: actions/setup-node@v6 27 | with: 28 | node-version: '24' 29 | registry-url: 'https://registry.npmjs.org' 30 | 31 | - run: bun run package --if-present 32 | 33 | - name: Publish package 34 | run: npm publish --provenance --access public 35 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gh-actions-lockfile-website", 3 | "private": true, 4 | "type": "module", 5 | "version": "0.0.1", 6 | "scripts": { 7 | "dev": "astro dev", 8 | "build": "astro build", 9 | "preview": "astro preview" 10 | }, 11 | "dependencies": { 12 | "@astrojs/mdx": "^4.3.13", 13 | "@astrojs/react": "^4.0.0", 14 | "@radix-ui/react-collapsible": "^1.1.0", 15 | "@radix-ui/react-slot": "^1.1.0", 16 | "@tailwindcss/typography": "^0.5.16", 17 | "@tailwindcss/vite": "^4.0.0", 18 | "astro": "^5.0.0", 19 | "class-variance-authority": "^0.7.0", 20 | "clsx": "^2.1.0", 21 | "lucide-react": "^0.561.0", 22 | "react": "^19.2.3", 23 | "react-dom": "^19.2.3", 24 | "tailwind-merge": "^3.4.0", 25 | "tailwindcss": "^4.1.18", 26 | "tw-animate-css": "^1.0.0" 27 | }, 28 | "devDependencies": { 29 | "@types/react": "^19.2.7", 30 | "@types/react-dom": "^19.2.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/commands/verify.ts: -------------------------------------------------------------------------------- 1 | import { join, dirname, isAbsolute } from "node:path"; 2 | import { readLockfile, verify, printVerifyResult } from "../lockfile/lockfile.js"; 3 | import { parseWorkflowDir } from "../parser/workflow.js"; 4 | import { findWorkflowDir } from "../utils/directory.js"; 5 | 6 | interface VerifyOptions { 7 | workflows: string; 8 | output: string; 9 | } 10 | 11 | export async function verifyCommand(options: VerifyOptions): Promise { 12 | const workflowDir = await findWorkflowDir(options.workflows); 13 | 14 | // Determine lockfile path 15 | let lockfilePath = options.output; 16 | if (!isAbsolute(lockfilePath)) { 17 | const repoRoot = dirname(dirname(workflowDir)); 18 | lockfilePath = join(repoRoot, lockfilePath); 19 | } 20 | 21 | const lockfile = await readLockfile(lockfilePath); 22 | const workflows = await parseWorkflowDir(workflowDir); 23 | 24 | const result = verify(workflows, lockfile); 25 | printVerifyResult(result); 26 | 27 | if (!result.match) { 28 | process.exit(1); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /website/src/components/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { ThemeToggle } from "./ThemeToggle"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Github } from "lucide-react"; 5 | 6 | interface Props { 7 | githubUrl: string; 8 | } 9 | 10 | const { githubUrl } = Astro.props; 11 | --- 12 | 13 |
14 | 33 |
34 | -------------------------------------------------------------------------------- /website/src/components/docs/DocsSidebar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { docsNavigation } from "@/lib/docs-config"; 3 | 4 | interface Props { 5 | currentPath: string; 6 | } 7 | 8 | const { currentPath } = Astro.props; 9 | 10 | function isActive(href: string): boolean { 11 | return currentPath === href || currentPath === href + "/"; 12 | } 13 | --- 14 | 15 | 39 | -------------------------------------------------------------------------------- /.github/workflows/deploy-website.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Website to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths: 7 | - "website/**" 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: false 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v6 24 | 25 | - name: Set up Bun 26 | uses: gjtorikian/actions/setup-languages/bun@main 27 | 28 | - name: Build website 29 | run: bun run docs:build 30 | 31 | - name: Setup Pages 32 | uses: actions/configure-pages@v5 33 | 34 | - name: Upload artifact 35 | uses: actions/upload-pages-artifact@v4 36 | with: 37 | path: website/dist 38 | 39 | deploy: 40 | environment: 41 | name: github-pages 42 | url: ${{ steps.deployment.outputs.page_url }} 43 | runs-on: ubuntu-latest 44 | needs: build 45 | steps: 46 | - name: Deploy to GitHub Pages 47 | id: deployment 48 | uses: actions/deploy-pages@v4 49 | -------------------------------------------------------------------------------- /src/utils/directory.ts: -------------------------------------------------------------------------------- 1 | import { stat } from "node:fs/promises"; 2 | import { dirname, isAbsolute, join } from "node:path"; 3 | 4 | export async function findWorkflowDir(dir: string): Promise { 5 | if (isAbsolute(dir)) { 6 | if (await directoryExists(dir)) { 7 | return dir; 8 | } 9 | throw new Error(`Workflow directory not found: ${dir}`); 10 | } 11 | 12 | const cwd = process.cwd(); 13 | 14 | // Try current directory 15 | let path = join(cwd, dir); 16 | if (await directoryExists(path)) { 17 | return path; 18 | } 19 | 20 | // Try to find .github in parent directories 21 | let current = cwd; 22 | while (true) { 23 | path = join(current, ".github", "workflows"); 24 | if (await directoryExists(path)) { 25 | return path; 26 | } 27 | 28 | const parent = dirname(current); 29 | if (parent === current) { 30 | break; 31 | } 32 | current = parent; 33 | } 34 | 35 | throw new Error(`Workflow directory not found: ${dir} (searched from ${cwd})`); 36 | } 37 | 38 | export async function directoryExists(path: string): Promise { 39 | try { 40 | const stats = await stat(path); 41 | return stats.isDirectory(); 42 | } catch { 43 | return false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.0](https://github.com/gjtorikian/gh-actions-lockfile/compare/v0.0.4...v1.0.0) (2025-12-16) 4 | 5 | 6 | ### ⚠ BREAKING CHANGES 7 | 8 | * consider that multiple versions of an action can exist 9 | 10 | ### Features 11 | 12 | * consider that multiple versions of an action can exist ([7e3c878](https://github.com/gjtorikian/gh-actions-lockfile/commit/7e3c87856e7393aa85124455b83a16568c3ecf8d)) 13 | 14 | ## [0.0.4](https://github.com/gjtorikian/gh-actions-lockfile/compare/v0.0.3...v0.0.4) (2025-12-16) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * whoops, DRY up copy-paste ([4c3e1da](https://github.com/gjtorikian/gh-actions-lockfile/commit/4c3e1da8a547abdb192c5c0d3bfea90fe09680e2)) 20 | 21 | ## [0.0.3](https://github.com/gjtorikian/gh-actions-lockfile/compare/v0.0.2...v0.0.3) (2025-12-16) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * special notice for rate limit errors ([f2e69a2](https://github.com/gjtorikian/gh-actions-lockfile/commit/f2e69a283e757a8e508a29b4c1863f54b2e18583)) 27 | 28 | ## [0.0.2](https://github.com/gjtorikian/gh-actions-lockfile/compare/v0.0.1...v0.0.2) (2025-12-16) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * this package was unused ([e3a0149](https://github.com/gjtorikian/gh-actions-lockfile/commit/e3a0149b7ce6ec9d30ff31efdc54dddbe47f223a)) 34 | -------------------------------------------------------------------------------- /website/src/components/BaseLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import "@/styles/globals.css"; 3 | 4 | interface Props { 5 | title: string; 6 | description?: string; 7 | } 8 | 9 | const { title, description = "Generate and verify lockfiles for GitHub Actions dependencies. Pin all actions to exact commit SHAs with integrity hashes." } = Astro.props; 10 | --- 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {title} 21 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /release.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | This project uses [release-please](https://github.com/googleapis/release-please) to automate releases based on [Conventional Commits](https://www.conventionalcommits.org/). 4 | 5 | ## Commit Message Format 6 | 7 | Commit messages determine how versions are bumped: 8 | 9 | | Commit Type | Example | Version Bump | 10 | | ------------------------------ | ----------------------------- | --------------------- | 11 | | `fix:` | `fix: resolve editor crash` | Patch (0.1.0 → 0.1.1) | 12 | | `feat:` | `feat: add dark mode support` | Minor (0.1.0 → 0.2.0) | 13 | | `feat!:` or `BREAKING CHANGE:` | `feat!: redesign API` | Major (0.1.0 → 1.0.0) | 14 | | `docs:`, `chore:`, `test:` | `docs: update README` | No release | 15 | 16 | ## Release Flow 17 | 18 | 1. **Create a feature branch** and make changes 19 | 2. **Use conventional commit messages** for all commits 20 | 3. **Open a PR** and merge to `main` 21 | 4. **release-please creates a Release PR** automatically 22 | - This PR accumulates changes and updates the changelog 23 | - Multiple merges to `main` will update the same Release PR 24 | 5. **Review and merge the Release PR** when ready to release 25 | 6. **A GitHub Release is created** with a git tag 26 | 7. **The publish workflow triggers** and publishes to npm 27 | 28 | ## Workflows 29 | 30 | ### `release-please.yml` 31 | 32 | - Triggers on every push to `main` 33 | - Creates/updates a Release PR with pending changes 34 | - When the Release PR is merged, creates a GitHub Release and git tag 35 | 36 | ### `publish.yml` 37 | 38 | - Triggers when a GitHub Release is published 39 | - Builds the package and publishes to npm with provenance 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gh-actions-lockfile", 3 | "version": "1.0.0", 4 | "license": "AGPL-3.0-or-later", 5 | "description": "Generate and verify lockfiles for GitHub Actions dependencies", 6 | "author": "Garen Torikian", 7 | "workspaces": [ 8 | "website" 9 | ], 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/gjtorikian/gh-actions-lockfile.git" 13 | }, 14 | "homepage": "https://gh-actions-lockfile.net", 15 | "bugs": { 16 | "url": "https://github.com/gjtorikian/gh-actions-lockfile/issues" 17 | }, 18 | "keywords": [ 19 | "github-actions", 20 | "lockfile", 21 | "security", 22 | "dependency-management", 23 | "ci-cd" 24 | ], 25 | "files": [ 26 | "dist" 27 | ], 28 | "type": "module", 29 | "types": "./dist/types/index.d.ts", 30 | "exports": { 31 | ".": { 32 | "types": "./dist/types/index.d.ts", 33 | "import": "./dist/cli.js" 34 | } 35 | }, 36 | "bin": { 37 | "gh-actions-lockfile": "./dist/cli.js" 38 | }, 39 | "scripts": { 40 | "start": "bun run src/index.ts", 41 | "build": "bun build src/index.ts --outfile dist/cli.js --target node && tsc --emitDeclarationOnly", 42 | "typecheck": "tsc --noEmit", 43 | "test": "bun test", 44 | "lint": "oxlint", 45 | "lint:fix": "oxlint --fix", 46 | "package": "bun run build && bun pm pack", 47 | "docs:dev": "bun run --filter gh-actions-lockfile-website dev", 48 | "docs:build": "bun run --filter gh-actions-lockfile-website build", 49 | "docs:preview": "bun run --filter gh-actions-lockfile-website preview" 50 | }, 51 | "dependencies": { 52 | "commander": "^14.0.2", 53 | "yaml": "^2.6.1" 54 | }, 55 | "devDependencies": { 56 | "@types/bun": "latest", 57 | "oxlint": "^1.33.0", 58 | "typescript": "^5.7.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/generate.ts: -------------------------------------------------------------------------------- 1 | import { join, dirname, isAbsolute } from "node:path"; 2 | import { GitHubClient } from "../github/client.js"; 3 | import { writeLockfile } from "../lockfile/lockfile.js"; 4 | import { extractActionRefs, parseWorkflowDir } from "../parser/workflow.js"; 5 | import { Resolver } from "../resolver/resolver.js"; 6 | import { findWorkflowDir } from "../utils/directory.js"; 7 | 8 | interface GenerateOptions { 9 | workflows: string; 10 | output: string; 11 | token?: string; 12 | } 13 | 14 | export async function generate(options: GenerateOptions): Promise { 15 | const workflowDir = await findWorkflowDir(options.workflows); 16 | 17 | console.log(`Parsing workflows from ${workflowDir}...`); 18 | 19 | const workflows = await parseWorkflowDir(workflowDir); 20 | 21 | if (workflows.length === 0) { 22 | throw new Error(`No workflow files found in ${workflowDir}`); 23 | } 24 | 25 | console.log(`Found ${workflows.length} workflow file(s)\n`); 26 | 27 | const refs = extractActionRefs(workflows); 28 | 29 | if (refs.length === 0) { 30 | console.log("No action references found in workflows"); 31 | return; 32 | } 33 | 34 | console.log(`Found ${refs.length} unique action reference(s)\n`); 35 | 36 | const client = new GitHubClient(options.token); 37 | const resolver = new Resolver(client); 38 | 39 | const lockfile = await resolver.resolveAll(refs); 40 | 41 | // Determine output path 42 | let outputPath = options.output; 43 | if (!isAbsolute(outputPath)) { 44 | // Make relative to repo root (parent of .github) 45 | const repoRoot = dirname(dirname(workflowDir)); 46 | outputPath = join(repoRoot, outputPath); 47 | } 48 | 49 | await writeLockfile(lockfile, outputPath); 50 | 51 | console.log(`\n✓ Lockfile written to ${outputPath}`); 52 | console.log(` ${Object.keys(lockfile.actions).length} action(s) locked`); 53 | } 54 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'GitHub Actions Lockfile' 2 | description: 'Generate and verify lockfiles for GitHub Actions dependencies' 3 | author: 'gjtorikian' 4 | 5 | branding: 6 | icon: 'lock' 7 | color: 'blue' 8 | 9 | inputs: 10 | mode: 11 | description: 'Mode to run in: generate or verify' 12 | required: true 13 | default: 'verify' 14 | token: 15 | description: 'GitHub token for API access' 16 | required: false 17 | default: ${{ github.token }} 18 | workflows: 19 | description: 'Path to workflows directory' 20 | required: false 21 | default: '.github/workflows' 22 | output: 23 | description: 'Path to lockfile' 24 | required: false 25 | default: '.github/workflows/actions.lock.json' 26 | 27 | outputs: 28 | changed: 29 | description: 'Whether the lockfile changed (for generate mode)' 30 | value: ${{ steps.run.outputs.changed }} 31 | 32 | runs: 33 | using: 'composite' 34 | steps: 35 | - name: Setup Bun 36 | uses: oven-sh/setup-bun@v2 37 | with: 38 | bun-version: latest 39 | cache: true 40 | 41 | - name: Install dependencies 42 | shell: bash 43 | working-directory: ${{ github.action_path }} 44 | run: bun install --frozen-lockfile 45 | 46 | - name: Run gh-actions-lockfile 47 | id: run 48 | shell: bash 49 | env: 50 | GITHUB_TOKEN: ${{ inputs.token }} 51 | run: | 52 | bun run ${{ github.action_path }}/src/index.ts ${{ inputs.mode }} \ 53 | --workflows "${{ inputs.workflows }}" \ 54 | --output "${{ inputs.output }}" 55 | 56 | # Check if lockfile changed (for generate mode) 57 | if [ "${{ inputs.mode }}" = "generate" ]; then 58 | if git diff --quiet "${{ inputs.output }}" 2>/dev/null; then 59 | echo "changed=false" >> $GITHUB_OUTPUT 60 | else 61 | echo "changed=true" >> $GITHUB_OUTPUT 62 | fi 63 | fi 64 | -------------------------------------------------------------------------------- /src/__fixtures__/helpers.ts: -------------------------------------------------------------------------------- 1 | import { copyFile, mkdir, readFile } from "node:fs/promises"; 2 | import { join, dirname } from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | 5 | // Base path for fixtures 6 | const __dirname = dirname(fileURLToPath(import.meta.url)); 7 | export const FIXTURES_DIR = __dirname; 8 | 9 | /** 10 | * Copy a fixture file to a target directory 11 | * @param fixturePath - Relative path from __fixtures__ (e.g., "verify/workflow-match.yml") 12 | * @param targetPath - Absolute path where fixture should be copied 13 | */ 14 | export async function copyFixture( 15 | fixturePath: string, 16 | targetPath: string 17 | ): Promise { 18 | const sourcePath = join(FIXTURES_DIR, fixturePath); 19 | await mkdir(dirname(targetPath), { recursive: true }); 20 | await copyFile(sourcePath, targetPath); 21 | } 22 | 23 | /** 24 | * Copy multiple fixtures to a target directory 25 | * @param fixtures - Array of [fixturePath, targetFileName] tuples 26 | * @param targetDir - Target directory (will be created if needed) 27 | */ 28 | export async function copyFixtures( 29 | fixtures: Array<[string, string]>, 30 | targetDir: string 31 | ): Promise { 32 | await mkdir(targetDir, { recursive: true }); 33 | await Promise.all( 34 | fixtures.map(([fixturePath, targetFileName]) => 35 | copyFixture(fixturePath, join(targetDir, targetFileName)) 36 | ) 37 | ); 38 | } 39 | 40 | /** 41 | * Read a fixture file as a string 42 | * @param fixturePath - Relative path from __fixtures__ 43 | */ 44 | export async function readFixture(fixturePath: string): Promise { 45 | const sourcePath = join(FIXTURES_DIR, fixturePath); 46 | return readFile(sourcePath, "utf-8"); 47 | } 48 | 49 | /** 50 | * Read a JSON fixture and parse it 51 | * @param fixturePath - Relative path from __fixtures__ 52 | */ 53 | export async function readJsonFixture(fixturePath: string): Promise { 54 | const content = await readFixture(fixturePath); 55 | return JSON.parse(content) as T; 56 | } 57 | -------------------------------------------------------------------------------- /website/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { Slot } from "@radix-ui/react-slot"; 3 | import { cva, type VariantProps } from "class-variance-authority"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ); 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean; 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button"; 45 | return ( 46 | 51 | ); 52 | } 53 | ); 54 | Button.displayName = "Button"; 55 | 56 | export { Button, buttonVariants }; 57 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // Workflow types 2 | export interface Workflow { 3 | name?: string; 4 | on?: unknown; 5 | jobs: Record; 6 | } 7 | 8 | export interface Job { 9 | name?: string; 10 | "runs-on"?: string; 11 | steps?: Step[]; 12 | uses?: string; 13 | } 14 | 15 | export interface Step { 16 | name?: string; 17 | uses?: string; 18 | with?: Record; 19 | run?: string; 20 | } 21 | 22 | // Action reference parsed from "uses" field 23 | export interface ActionRef { 24 | owner: string; 25 | repo: string; 26 | ref: string; 27 | path?: string; 28 | rawUses: string; 29 | } 30 | 31 | export interface Lockfile { 32 | version: number; 33 | generated: string; 34 | actions: Record; 35 | } 36 | 37 | export interface LockedAction { 38 | version: string; 39 | sha: string; 40 | integrity: string; 41 | dependencies: LockedDependency[]; 42 | } 43 | 44 | export interface LockedDependency { 45 | ref: string; 46 | sha: string; 47 | integrity: string; 48 | } 49 | 50 | export interface VerifyResult { 51 | match: boolean; 52 | newActions: ChangeInfo[]; 53 | changed: ChangeInfo[]; 54 | removed: ChangeInfo[]; 55 | } 56 | 57 | export interface ChangeInfo { 58 | action: string; 59 | oldVersion?: string; 60 | newVersion?: string; 61 | oldSha?: string; 62 | newSha?: string; 63 | } 64 | 65 | // GitHub API response types 66 | export interface GitRef { 67 | ref: string; 68 | object: { 69 | sha: string; 70 | type: string; 71 | }; 72 | } 73 | 74 | export interface GitTag { 75 | object: { 76 | sha: string; 77 | type: string; 78 | }; 79 | } 80 | 81 | export interface FileContent { 82 | encoding: string; 83 | content: string; 84 | download_url: string; 85 | } 86 | 87 | // Action.yml structure 88 | export interface ActionConfig { 89 | name?: string; 90 | description?: string; 91 | runs?: { 92 | using?: string; 93 | steps?: Array<{ 94 | uses?: string; 95 | with?: Record; 96 | run?: string; 97 | }>; 98 | }; 99 | jobs?: Record; 100 | } 101 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bun 2 | import { Command } from "commander"; 3 | import { generate } from "./commands/generate.js"; 4 | import { verifyCommand } from "./commands/verify.js"; 5 | import { list } from "./commands/list.js"; 6 | import { DEFAULT_PATH } from "./lockfile/lockfile.js"; 7 | import pkg from "../package.json"; 8 | 9 | const program = new Command(); 10 | 11 | program 12 | .name("gh-actions-lockfile") 13 | .description( 14 | "Generate and verify lockfiles for GitHub Actions dependencies." 15 | ) 16 | .version(pkg.version); 17 | 18 | // Global options 19 | const workflowsOption = [ 20 | "-w, --workflows ", 21 | "Path to workflows directory", 22 | ".github/workflows", 23 | ] as const; 24 | 25 | const outputOption = ["-o, --output ", "Path to lockfile", DEFAULT_PATH] as const; 26 | 27 | const tokenOption = [ 28 | "-t, --token ", 29 | "GitHub token (or use GITHUB_TOKEN env var)", 30 | ] as const; 31 | 32 | program 33 | .command("generate") 34 | .description("Generate or update the lockfile") 35 | .option(...workflowsOption) 36 | .option(...outputOption) 37 | .option(...tokenOption) 38 | .action(async (options) => { 39 | try { 40 | await generate(options); 41 | } catch (error) { 42 | console.error("Error:", error instanceof Error ? error.message : error); 43 | process.exit(2); 44 | } 45 | }); 46 | 47 | program 48 | .command("verify") 49 | .description("Verify workflows match the lockfile") 50 | .option(...workflowsOption) 51 | .option(...outputOption) 52 | .action(async (options) => { 53 | try { 54 | await verifyCommand(options); 55 | } catch (error) { 56 | console.error("Error:", error instanceof Error ? error.message : error); 57 | process.exit(2); 58 | } 59 | }); 60 | 61 | program 62 | .command("list") 63 | .description("Display the dependency tree of all locked actions") 64 | .option(...workflowsOption) 65 | .option(...outputOption) 66 | .action(async (options) => { 67 | try { 68 | await list(options); 69 | } catch (error) { 70 | console.error("Error:", error instanceof Error ? error.message : error); 71 | process.exit(2); 72 | } 73 | }); 74 | 75 | program.parse(); 76 | -------------------------------------------------------------------------------- /website/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
44 | )); 45 | CardTitle.displayName = "CardTitle"; 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLDivElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )); 57 | CardDescription.displayName = "CardDescription"; 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |
64 | )); 65 | CardContent.displayName = "CardContent"; 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )); 77 | CardFooter.displayName = "CardFooter"; 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; 80 | -------------------------------------------------------------------------------- /website/src/content/docs/commands.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Commands 3 | description: Reference for all gh-actions-lockfile commands. 4 | order: 3 5 | --- 6 | 7 | # Commands 8 | 9 | gh-actions-lockfile provides three main commands for managing your lockfile. 10 | 11 | ## generate 12 | 13 | Generates (or updates) the lockfile from your workflow files. Run this first to create your initial lockfile, and again whenever you intentionally update action versions. 14 | 15 | ```bash 16 | node dist/cli.js generate 17 | ``` 18 | 19 | This scans all workflow files in `.github/workflows/` and creates `actions.lock.json` with pinned versions for every action, including transitive dependencies from composite actions. 20 | 21 | ## verify 22 | 23 | Verifies that your workflow files match the lockfile. Use this in CI to catch any unexpected changes to your actions. 24 | 25 | ```bash 26 | node dist/cli.js verify 27 | ``` 28 | 29 | Exit codes: 30 | - `0` - All actions match the lockfile 31 | - `1` - Mismatch detected (action added, removed, or changed) 32 | 33 | When verification fails, the output shows exactly what changed. 34 | 35 | ## list 36 | 37 | Visualizes the actions dependency structure as a tree. Useful for understanding what actions your workflows depend on, including transitive dependencies. 38 | 39 | ```bash 40 | node dist/cli.js list 41 | ``` 42 | 43 | Example output: 44 | 45 | ``` 46 | actions.lock.json (generated 2025-12-15 21:57:33) 47 | 48 | ├── actions/checkout@v6 (8e8c483db84b) 49 | ├── gjtorikian/actions/setup-languages@main (923ecf42f98c) 50 | │ ├── ruby/setup-ruby@v1 (ac793fdd38cc) 51 | │ ├── actions/setup-node@v4 (49933ea5288c) 52 | │ ├── denoland/setup-deno@v1 (11b63cf76cfc) 53 | │ ├── dtolnay/rust-toolchain@master (0b1efabc08b6) 54 | │ └── Swatinem/rust-cache@v2 (779680da715d) 55 | ├── actions/cache@v4 (0057852bfaa8) 56 | ├── actions/configure-pages@v4 (1f0c5cde4bc7) 57 | ├── actions/upload-pages-artifact@v3 (56afc609e742) 58 | │ └── actions/upload-artifact@v4 (ea165f8d65b6) 59 | ├── actions/deploy-pages@v4 (d6db90164ac5) 60 | └── googleapis/release-please-action@v4 (16a9c90856f4) 61 | ``` 62 | 63 | The tree shows: 64 | - Each action with its version tag 65 | - The short SHA (first 12 characters) of the pinned commit 66 | - Transitive dependencies indented under their parent action 67 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # database 10 | *.sqlite 11 | 12 | # package-json 13 | package-json.lock 14 | 15 | # bun deploy file 16 | node_modules.bun 17 | 18 | # Diagnostic reports (https://nodejs.org/api/report.html) 19 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 20 | 21 | # Runtime data 22 | pids 23 | *.pid 24 | *.seed 25 | *.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | lib-cov 29 | 30 | # Coverage directory used by tools like istanbul 31 | coverage 32 | *.lcov 33 | 34 | # nyc test coverage 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | .grunt 39 | 40 | # Bower dependency directory (https://bower.io/) 41 | bower_components 42 | 43 | # node-waf configuration 44 | .lock-wscript 45 | 46 | # Compiled binary addons (https://nodejs.org/api/addons.html) 47 | build/Release 48 | 49 | # Dependency directories 50 | node_modules/ 51 | jspm_packages/ 52 | 53 | # TypeScript v1 declaration files 54 | typings/ 55 | 56 | # TypeScript cache 57 | *.tsbuildinfo 58 | 59 | # Optional npm cache directory 60 | .npm 61 | 62 | # Optional eslint cache 63 | .eslintcache 64 | 65 | # Microbundle cache 66 | .rpt2_cache/ 67 | .rts2_cache_cjs/ 68 | .rts2_cache_es/ 69 | .rts2_cache_umd/ 70 | 71 | # Optional REPL history 72 | .node_repl_history 73 | 74 | # Output of 'npm pack' 75 | *.tgz 76 | 77 | # Yarn Integrity file 78 | .yarn-integrity 79 | 80 | # dotenv environment variables file 81 | .env 82 | .env.test 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | 87 | # Next.js build output 88 | .next 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Astro build output 95 | .astro/ 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # Serverless directories 107 | .serverless/ 108 | 109 | # FuseBox cache 110 | .fusebox/ 111 | 112 | # DynamoDB Local files 113 | .dynamodb/ 114 | 115 | # TernJS port file 116 | .tern-port 117 | 118 | *.bun-build 119 | -------------------------------------------------------------------------------- /website/src/content/docs/cli-reference.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: CLI Reference 3 | description: Complete reference for gh-actions-lockfile CLI options and environment variables. 4 | order: 4 5 | --- 6 | 7 | import GitHubTokenNote from "../../components/docs/GitHubTokenNote.astro"; 8 | 9 | # CLI Reference 10 | 11 | Complete reference for all CLI options and environment variables. 12 | 13 | 14 | 15 | ## Options 16 | 17 | All commands accept the following options: 18 | 19 | ### `-w, --workflows ` 20 | 21 | Path to the workflows directory. 22 | 23 | **Default:** `.github/workflows` 24 | 25 | ```bash 26 | node dist/cli.js generate --workflows ./my-workflows 27 | ``` 28 | 29 | ### `-o, --output ` 30 | 31 | Path to the lockfile. 32 | 33 | **Default:** `.github/workflows/actions.lock.json` 34 | 35 | ```bash 36 | node dist/cli.js generate --output ./lockfile.json 37 | ``` 38 | 39 | ### `-t, --token ` 40 | 41 | GitHub token for API authentication. Required for private repositories or to avoid rate limiting. 42 | 43 | ```bash 44 | node dist/cli.js generate --token ghp_xxxxxxxxxxxx 45 | ``` 46 | 47 | ## Environment Variables 48 | 49 | ### `GITHUB_TOKEN` 50 | 51 | Alternative to the `--token` option. If both are provided, the command-line option takes precedence. 52 | 53 | ```bash 54 | export GITHUB_TOKEN=ghp_xxxxxxxxxxxx 55 | node dist/cli.js generate 56 | ``` 57 | 58 | In GitHub Actions, this is automatically available: 59 | 60 | ```yaml 61 | - uses: gjtorikian/gh-actions-lockfile@v1 62 | with: 63 | mode: generate 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | ``` 67 | 68 | ## Running with Different Runtimes 69 | 70 | ### Node.js (Recommended) 71 | 72 | The built CLI works with Node.js without additional dependencies: 73 | 74 | ```bash 75 | node dist/cli.js [options] 76 | ``` 77 | 78 | ### Bun 79 | 80 | You can also run directly from source with Bun: 81 | 82 | ```bash 83 | bun run src/index.ts [options] 84 | ``` 85 | 86 | ## Examples 87 | 88 | Generate a lockfile with custom paths: 89 | 90 | ```bash 91 | node dist/cli.js generate \ 92 | --workflows ./workflows \ 93 | --output ./workflows/actions.lock.json 94 | ``` 95 | 96 | Verify in CI with explicit token: 97 | 98 | ```bash 99 | GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} node dist/cli.js verify 100 | ``` 101 | 102 | List dependencies for a specific lockfile: 103 | 104 | ```bash 105 | node dist/cli.js list --output ./custom-lockfile.json 106 | ``` 107 | -------------------------------------------------------------------------------- /website/src/components/docs/DocsLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseLayout from "../BaseLayout.astro"; 3 | import Header from "../Header.astro"; 4 | import Footer from "../Footer.astro"; 5 | import DocsSidebar from "./DocsSidebar.astro"; 6 | import { MobileNav } from "./MobileNav"; 7 | import { githubUrl } from "@/lib/docs-config"; 8 | 9 | interface Props { 10 | title: string; 11 | description?: string; 12 | currentPath: string; 13 | } 14 | 15 | const { title, description, currentPath } = Astro.props; 16 | const pageTitle = `${title} | gh-actions-lockfile`; 17 | const pageDescription = description || `${title} - gh-actions-lockfile documentation`; 18 | --- 19 | 20 | 21 |
22 | 23 |
24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 |
39 |
40 |
41 | 42 |