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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/check-pinned-actions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Check pinned GitHub Action versions

on:
pull_request:
paths:
- '**/*.yml'
- '**/*.yaml'

jobs:
check:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- uses: shopware/github-actions/check-pinned-actions@main
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ A collection of reusable GitHub Actions and Workflows for Shopware extensions an
| [downstream](downstream/) | Triggers a downstream workflow and waits for it to finish | [README](downstream/README.md) |
| [upstream-connect](upstream-connect/) | Connects to upstream from downstream run | [README](upstream-connect/README.md) |

### Action Pinning

| Action | Description | Link |
|--------|-------------|------|
| [check-pinned-actions](check-pinned-actions/) | Fails if any non-Shopware action is not pinned to a commit SHA — use as a PR gate | [README](check-pinned-actions/README.md) |

### Deployment

| Action | Description | Link |
Expand Down
30 changes: 30 additions & 0 deletions check-pinned-actions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# check-pinned-actions

Fails if any non-Shopware GitHub Action in the repository is not pinned to a full commit SHA. Shopware-owned and local (`./`) actions are skipped.

Intended as a PR gate to prevent unpinned actions from being merged.

## Usage

```yaml
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: shopware/github-actions/check-pinned-actions@main
```

## No inputs

This action takes no inputs. It exits with a non-zero status and prints each violation if unpinned actions are found, e.g.:

```
Found 2 unpinned action(s):

.github/workflows/ci.yml:12 actions/checkout@v4
.github/workflows/ci.yml:18 codecov/codecov-action@v3

Pin each action to a full commit SHA, e.g.:
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
```
9 changes: 9 additions & 0 deletions check-pinned-actions/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: Check pinned GitHub Action versions
description: Fails if any non-Shopware GitHub Action is not pinned to a full commit SHA.

runs:
using: composite
steps:
- name: Check for unpinned actions
shell: bash
run: node "${{ github.action_path }}/check-pinned-actions.js"
61 changes: 61 additions & 0 deletions check-pinned-actions/check-pinned-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env node
'use strict';

const fs = require('fs');
const path = require('path');

const ACTION_RE = /uses:\s+([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+)@([a-zA-Z0-9._/-]+)(?:\s*#\s*(\S+))?/g;
const SHA_RE = /^[0-9a-f]{40}$/;

function findYamlFiles(root = '.') {
const results = [];
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
const full = path.join(root, entry.name);
if (entry.isDirectory()) {
results.push(...findYamlFiles(full));
} else if (entry.name.endsWith('.yml') || entry.name.endsWith('.yaml')) {
results.push(full);
}
}
return results;
}

function isThirdParty(action) {
const owner = action.split('/')[0];
return owner !== 'shopware' && !owner.startsWith('.');
}

function main() {
const yamlFiles = findYamlFiles();
const violations = [];

for (const file of yamlFiles) {
const content = fs.readFileSync(file, 'utf8');
const lines = content.split('\n');

for (let i = 0; i < lines.length; i++) {
for (const [, action, ref] of lines[i].matchAll(ACTION_RE)) {
if (!isThirdParty(action)) continue;

if (!SHA_RE.test(ref)) {
violations.push({ file, line: i + 1, action, ref });
}
}
}
}

if (violations.length === 0) {
console.log('All third-party actions are pinned to a commit SHA.');
return;
}

console.error(`\nFound ${violations.length} unpinned action(s):\n`);
for (const { file, line, action, ref } of violations) {
console.error(` ${file}:${line} ${action}@${ref}`);
}
console.error('\nPin each action to a full commit SHA, e.g.:');
console.error(' uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n');
process.exit(1);
}

main();
Loading