mirror of
https://github.com/github/codeql-action.git
synced 2026-04-02 17:52:19 +00:00
Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/npm-minor-af60a9b329
This commit is contained in:
7
.github/workflows/pr-checks.yml
vendored
7
.github/workflows/pr-checks.yml
vendored
@@ -42,11 +42,6 @@ jobs:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
# Use the system Bash shell to ensure we can run commands like `npm ci`
|
||||
@@ -68,7 +63,7 @@ jobs:
|
||||
- name: Run pr-checks tests
|
||||
if: always()
|
||||
working-directory: pr-checks
|
||||
run: python -m unittest discover
|
||||
run: npm ci && npx tsx --test
|
||||
|
||||
- name: Lint
|
||||
if: always() && matrix.os != 'windows-latest'
|
||||
|
||||
8
.github/workflows/rebuild.yml
vendored
8
.github/workflows/rebuild.yml
vendored
@@ -73,17 +73,13 @@ jobs:
|
||||
npm run lint -- --fix
|
||||
npm run build
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: 3.11
|
||||
|
||||
- name: Sync back version updates to generated workflows
|
||||
# Only sync back versions on Dependabot update PRs
|
||||
if: startsWith(env.HEAD_REF, 'dependabot/')
|
||||
working-directory: pr-checks
|
||||
run: |
|
||||
python3 sync_back.py -v
|
||||
npm ci
|
||||
npx tsx sync_back.ts --verbose
|
||||
|
||||
- name: Generate workflows
|
||||
working-directory: pr-checks
|
||||
|
||||
3
pr-checks/.gitignore
vendored
3
pr-checks/.gitignore
vendored
@@ -1,4 +1 @@
|
||||
env
|
||||
__pycache__/
|
||||
*.pyc
|
||||
node_modules/
|
||||
|
||||
@@ -6,9 +6,9 @@ to one of the files in this directory.
|
||||
|
||||
## Updating workflows
|
||||
|
||||
Run `./sync.sh` to invoke the workflow generator and re-generate the workflow files in `.github/workflows/` based on the templates in `pr-checks/checks/`.
|
||||
|
||||
Alternatively, you can use `just`:
|
||||
|
||||
1. Install https://github.com/casey/just by whichever way you prefer.
|
||||
2. Run `just update-pr-checks` in your terminal.
|
||||
|
||||
### If you don't want to install `just`
|
||||
|
||||
Manually run each step in the `justfile`.
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Sync-back script to automatically update action versions in source templates
|
||||
from the generated workflow files after Dependabot updates.
|
||||
|
||||
This script scans the generated workflow files (.github/workflows/__*.yml) to find
|
||||
all external action versions used, then updates:
|
||||
1. Hardcoded action versions in pr-checks/sync.py
|
||||
2. Action version references in template files in pr-checks/checks/
|
||||
|
||||
The script automatically detects all actions used in generated workflows and
|
||||
preserves version comments (e.g., # v1.2.3) when syncing versions.
|
||||
|
||||
This ensures that when Dependabot updates action versions in generated workflows,
|
||||
those changes are properly synced back to the source templates. Regular workflow
|
||||
files are updated directly by Dependabot and don't need sync-back.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import glob
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
def scan_generated_workflows(workflow_dir: str) -> Dict[str, str]:
|
||||
"""
|
||||
Scan generated workflow files to extract the latest action versions.
|
||||
|
||||
Args:
|
||||
workflow_dir: Path to .github/workflows directory
|
||||
|
||||
Returns:
|
||||
Dictionary mapping action names to their latest versions (including comments)
|
||||
"""
|
||||
action_versions = {}
|
||||
generated_files = glob.glob(os.path.join(workflow_dir, "__*.yml"))
|
||||
|
||||
for file_path in generated_files:
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Find all action uses in the file, including potential comments
|
||||
# This pattern captures: action_name@version_with_possible_comment
|
||||
pattern = r'uses:\s+([^/\s]+/[^@\s]+)@([^@\n]+)'
|
||||
matches = re.findall(pattern, content)
|
||||
|
||||
for action_name, version_with_comment in matches:
|
||||
# Only track non-local actions (those with / but not starting with ./)
|
||||
if not action_name.startswith('./'):
|
||||
# Assume that version numbers are consistent (this should be the case on a Dependabot update PR)
|
||||
action_versions[action_name] = version_with_comment.rstrip()
|
||||
|
||||
return action_versions
|
||||
|
||||
|
||||
def update_sync_py(sync_py_path: str, action_versions: Dict[str, str]) -> bool:
|
||||
"""
|
||||
Update hardcoded action versions in pr-checks/sync.py
|
||||
|
||||
Args:
|
||||
sync_py_path: Path to sync.py file
|
||||
action_versions: Dictionary of action names to versions (may include comments)
|
||||
|
||||
Returns:
|
||||
True if file was modified, False otherwise
|
||||
"""
|
||||
if not os.path.exists(sync_py_path):
|
||||
raise FileNotFoundError(f"Could not find {sync_py_path}")
|
||||
|
||||
with open(sync_py_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
original_content = content
|
||||
|
||||
# Update hardcoded action versions
|
||||
for action_name, version_with_comment in action_versions.items():
|
||||
# Extract just the version part (before any comment) for sync.py
|
||||
version = version_with_comment.split('#')[0].strip() if '#' in version_with_comment else version_with_comment.strip()
|
||||
|
||||
# Look for patterns like 'uses': 'actions/setup-node@v4'
|
||||
# Note that this will break if we store an Action uses reference in a
|
||||
# variable - that's a risk we're happy to take since in that case the
|
||||
# PR checks will just fail.
|
||||
pattern = rf"('uses':\s*'){re.escape(action_name)}@(?:[^']+)(')"
|
||||
replacement = rf"\1{action_name}@{version}\2"
|
||||
content = re.sub(pattern, replacement, content)
|
||||
|
||||
if content != original_content:
|
||||
with open(sync_py_path, 'w') as f:
|
||||
f.write(content)
|
||||
print(f"Updated {sync_py_path}")
|
||||
return True
|
||||
else:
|
||||
print(f"No changes needed in {sync_py_path}")
|
||||
return False
|
||||
|
||||
|
||||
def update_template_files(checks_dir: str, action_versions: Dict[str, str]) -> List[str]:
|
||||
"""
|
||||
Update action versions in template files in pr-checks/checks/
|
||||
|
||||
Args:
|
||||
checks_dir: Path to pr-checks/checks directory
|
||||
action_versions: Dictionary of action names to versions (may include comments)
|
||||
|
||||
Returns:
|
||||
List of files that were modified
|
||||
"""
|
||||
modified_files = []
|
||||
template_files = glob.glob(os.path.join(checks_dir, "*.yml"))
|
||||
|
||||
for file_path in template_files:
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
original_content = content
|
||||
|
||||
# Update action versions
|
||||
for action_name, version_with_comment in action_versions.items():
|
||||
# Look for patterns like 'uses: actions/setup-node@v4' or 'uses: actions/setup-node@sha # comment'
|
||||
pattern = rf"(uses:\s+{re.escape(action_name)})@(?:[^@\n]+)"
|
||||
replacement = rf"\1@{version_with_comment}"
|
||||
content = re.sub(pattern, replacement, content)
|
||||
|
||||
if content != original_content:
|
||||
with open(file_path, 'w') as f:
|
||||
f.write(content)
|
||||
modified_files.append(file_path)
|
||||
print(f"Updated {file_path}")
|
||||
|
||||
return modified_files
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Sync action versions from generated workflows back to templates")
|
||||
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose output")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Get the repository root (assuming script is in pr-checks/)
|
||||
script_dir = Path(__file__).parent
|
||||
repo_root = script_dir.parent
|
||||
|
||||
workflow_dir = repo_root / ".github" / "workflows"
|
||||
checks_dir = script_dir / "checks"
|
||||
sync_py_path = script_dir / "sync.py"
|
||||
|
||||
print("Scanning generated workflows for latest action versions...")
|
||||
action_versions = scan_generated_workflows(str(workflow_dir))
|
||||
|
||||
if args.verbose:
|
||||
print("Found action versions:")
|
||||
for action, version in action_versions.items():
|
||||
print(f" {action}@{version}")
|
||||
|
||||
if not action_versions:
|
||||
print("No action versions found in generated workflows")
|
||||
return 1
|
||||
|
||||
# Update files
|
||||
print("\nUpdating source files...")
|
||||
modified_files = []
|
||||
|
||||
# Update sync.py
|
||||
if update_sync_py(str(sync_py_path), action_versions):
|
||||
modified_files.append(str(sync_py_path))
|
||||
|
||||
# Update template files
|
||||
template_modified = update_template_files(str(checks_dir), action_versions)
|
||||
modified_files.extend(template_modified)
|
||||
|
||||
if modified_files:
|
||||
print(f"\nSync completed. Modified {len(modified_files)} files:")
|
||||
for file_path in modified_files:
|
||||
print(f" {file_path}")
|
||||
else:
|
||||
print("\nNo files needed updating - all action versions are already in sync")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
250
pr-checks/sync_back.test.ts
Executable file
250
pr-checks/sync_back.test.ts
Executable file
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/*
|
||||
Tests for the sync_back.ts script
|
||||
*/
|
||||
|
||||
import * as assert from "node:assert/strict";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { afterEach, beforeEach, describe, it } from "node:test";
|
||||
|
||||
import {
|
||||
scanGeneratedWorkflows,
|
||||
updateSyncTs,
|
||||
updateTemplateFiles,
|
||||
} from "./sync_back";
|
||||
|
||||
let testDir: string;
|
||||
let workflowDir: string;
|
||||
let checksDir: string;
|
||||
let syncTsPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
/** Set up temporary directories and files for testing */
|
||||
testDir = fs.mkdtempSync(path.join(os.tmpdir(), "sync-back-test-"));
|
||||
workflowDir = path.join(testDir, ".github", "workflows");
|
||||
checksDir = path.join(testDir, "pr-checks", "checks");
|
||||
fs.mkdirSync(workflowDir, { recursive: true });
|
||||
fs.mkdirSync(checksDir, { recursive: true });
|
||||
|
||||
// Create sync.ts file path
|
||||
syncTsPath = path.join(testDir, "pr-checks", "sync.ts");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
/** Clean up temporary directories */
|
||||
fs.rmSync(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe("scanGeneratedWorkflows", () => {
|
||||
it("basic workflow scanning", () => {
|
||||
/** Test basic workflow scanning functionality */
|
||||
const workflowContent = `
|
||||
name: Test Workflow
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/setup-go@v6
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(workflowDir, "__test.yml"), workflowContent);
|
||||
|
||||
const result = scanGeneratedWorkflows(workflowDir);
|
||||
|
||||
assert.equal(result["actions/checkout"], "v4");
|
||||
assert.equal(result["actions/setup-node"], "v5");
|
||||
assert.equal(result["actions/setup-go"], "v6");
|
||||
});
|
||||
|
||||
it("scanning workflows with version comments", () => {
|
||||
/** Test scanning workflows with version comments */
|
||||
const workflowContent = `
|
||||
name: Test Workflow
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
- uses: actions/setup-python@v6 # Latest Python
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(workflowDir, "__test.yml"), workflowContent);
|
||||
|
||||
const result = scanGeneratedWorkflows(workflowDir);
|
||||
|
||||
assert.equal(result["actions/checkout"], "v4");
|
||||
assert.equal(
|
||||
result["ruby/setup-ruby"],
|
||||
"44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0",
|
||||
);
|
||||
assert.equal(result["actions/setup-python"], "v6 # Latest Python");
|
||||
});
|
||||
|
||||
it("ignores local actions", () => {
|
||||
/** Test that local actions (starting with ./) are ignored */
|
||||
const workflowContent = `
|
||||
name: Test Workflow
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/local-action
|
||||
- uses: ./another-local-action@v1
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(workflowDir, "__test.yml"), workflowContent);
|
||||
|
||||
const result = scanGeneratedWorkflows(workflowDir);
|
||||
|
||||
assert.equal(result["actions/checkout"], "v4");
|
||||
assert.equal("./.github/actions/local-action" in result, false);
|
||||
assert.equal("./another-local-action" in result, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateSyncTs", () => {
|
||||
it("updates sync.ts file", () => {
|
||||
/** Test updating sync.ts file */
|
||||
const syncTsContent = `
|
||||
const steps = [
|
||||
{
|
||||
uses: "actions/setup-node@v4",
|
||||
with: { "node-version": "16" },
|
||||
},
|
||||
{
|
||||
uses: "actions/setup-go@v5",
|
||||
with: { "go-version": "1.19" },
|
||||
},
|
||||
];
|
||||
`;
|
||||
|
||||
fs.writeFileSync(syncTsPath, syncTsContent);
|
||||
|
||||
const actionVersions = {
|
||||
"actions/setup-node": "v5",
|
||||
"actions/setup-go": "v6",
|
||||
};
|
||||
|
||||
const result = updateSyncTs(syncTsPath, actionVersions);
|
||||
assert.equal(result, true);
|
||||
|
||||
const updatedContent = fs.readFileSync(syncTsPath, "utf8");
|
||||
|
||||
assert.ok(updatedContent.includes('uses: "actions/setup-node@v5"'));
|
||||
assert.ok(updatedContent.includes('uses: "actions/setup-go@v6"'));
|
||||
});
|
||||
|
||||
it("strips comments from versions", () => {
|
||||
/** Test updating sync.ts file when versions have comments */
|
||||
const syncTsContent = `
|
||||
const steps = [
|
||||
{
|
||||
uses: "actions/setup-node@v4",
|
||||
with: { "node-version": "16" },
|
||||
},
|
||||
];
|
||||
`;
|
||||
|
||||
fs.writeFileSync(syncTsPath, syncTsContent);
|
||||
|
||||
const actionVersions = {
|
||||
"actions/setup-node": "v5 # Latest version",
|
||||
};
|
||||
|
||||
const result = updateSyncTs(syncTsPath, actionVersions);
|
||||
assert.equal(result, true);
|
||||
|
||||
const updatedContent = fs.readFileSync(syncTsPath, "utf8");
|
||||
|
||||
// sync.ts should get the version without comment
|
||||
assert.ok(updatedContent.includes('uses: "actions/setup-node@v5"'));
|
||||
assert.ok(!updatedContent.includes("# Latest version"));
|
||||
});
|
||||
|
||||
it("returns false when no changes are needed", () => {
|
||||
/** Test that updateSyncTs returns false when no changes are needed */
|
||||
const syncTsContent = `
|
||||
const steps = [
|
||||
{
|
||||
uses: "actions/setup-node@v5",
|
||||
with: { "node-version": "16" },
|
||||
},
|
||||
];
|
||||
`;
|
||||
|
||||
fs.writeFileSync(syncTsPath, syncTsContent);
|
||||
|
||||
const actionVersions = {
|
||||
"actions/setup-node": "v5",
|
||||
};
|
||||
|
||||
const result = updateSyncTs(syncTsPath, actionVersions);
|
||||
assert.equal(result, false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateTemplateFiles", () => {
|
||||
it("updates template files", () => {
|
||||
/** Test updating template files */
|
||||
const templateContent = `
|
||||
name: Test Template
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
`;
|
||||
|
||||
const templatePath = path.join(checksDir, "test.yml");
|
||||
fs.writeFileSync(templatePath, templateContent);
|
||||
|
||||
const actionVersions = {
|
||||
"actions/checkout": "v4",
|
||||
"actions/setup-node": "v5 # Latest",
|
||||
};
|
||||
|
||||
const result = updateTemplateFiles(checksDir, actionVersions);
|
||||
assert.equal(result.length, 1);
|
||||
assert.ok(result.includes(templatePath));
|
||||
|
||||
const updatedContent = fs.readFileSync(templatePath, "utf8");
|
||||
|
||||
assert.ok(updatedContent.includes("uses: actions/checkout@v4"));
|
||||
assert.ok(updatedContent.includes("uses: actions/setup-node@v5 # Latest"));
|
||||
});
|
||||
|
||||
it("preserves version comments", () => {
|
||||
/** Test that updating template files preserves version comments */
|
||||
const templateContent = `
|
||||
name: Test Template
|
||||
steps:
|
||||
- uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.256.0
|
||||
`;
|
||||
|
||||
const templatePath = path.join(checksDir, "test.yml");
|
||||
fs.writeFileSync(templatePath, templateContent);
|
||||
|
||||
const actionVersions = {
|
||||
"ruby/setup-ruby":
|
||||
"55511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0",
|
||||
};
|
||||
|
||||
const result = updateTemplateFiles(checksDir, actionVersions);
|
||||
assert.equal(result.length, 1);
|
||||
|
||||
const updatedContent = fs.readFileSync(templatePath, "utf8");
|
||||
|
||||
assert.ok(
|
||||
updatedContent.includes(
|
||||
"uses: ruby/setup-ruby@55511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0",
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
220
pr-checks/sync_back.ts
Executable file
220
pr-checks/sync_back.ts
Executable file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env npx tsx
|
||||
|
||||
/*
|
||||
Sync-back script to automatically update action versions in source templates
|
||||
from the generated workflow files after Dependabot updates.
|
||||
|
||||
This script scans the generated workflow files (.github/workflows/__*.yml) to find
|
||||
all external action versions used, then updates:
|
||||
1. Hardcoded action versions in pr-checks/sync.ts
|
||||
2. Action version references in template files in pr-checks/checks/
|
||||
|
||||
The script automatically detects all actions used in generated workflows and
|
||||
preserves version comments (e.g., # v1.2.3) when syncing versions.
|
||||
|
||||
This ensures that when Dependabot updates action versions in generated workflows,
|
||||
those changes are properly synced back to the source templates. Regular workflow
|
||||
files are updated directly by Dependabot and don't need sync-back.
|
||||
*/
|
||||
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const THIS_DIR = __dirname;
|
||||
const CHECKS_DIR = path.join(THIS_DIR, "checks");
|
||||
const WORKFLOW_DIR = path.join(THIS_DIR, "..", ".github", "workflows");
|
||||
const SYNC_TS_PATH = path.join(THIS_DIR, "sync.ts");
|
||||
|
||||
/**
|
||||
* Scan generated workflow files to extract the latest action versions.
|
||||
*
|
||||
* @param workflowDir - Path to .github/workflows directory
|
||||
* @returns Map from action names to their latest versions (including comments)
|
||||
*/
|
||||
export function scanGeneratedWorkflows(workflowDir: string): Record<string, string> {
|
||||
const actionVersions: Record<string, string> = {};
|
||||
|
||||
const generatedFiles = fs
|
||||
.readdirSync(workflowDir)
|
||||
.filter((f) => f.startsWith("__") && f.endsWith(".yml"))
|
||||
.map((f) => path.join(workflowDir, f));
|
||||
|
||||
for (const filePath of generatedFiles) {
|
||||
const content = fs.readFileSync(filePath, "utf8");
|
||||
|
||||
// Find all action uses in the file, including potential comments
|
||||
// This pattern captures: action_name@version_with_possible_comment
|
||||
const pattern = /uses:\s+([^/\s]+\/[^@\s]+)@([^@\n]+)/g;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = pattern.exec(content)) !== null) {
|
||||
const actionName = match[1];
|
||||
const versionWithComment = match[2].trimEnd();
|
||||
|
||||
// Only track non-local actions (those with / but not starting with ./)
|
||||
if (!actionName.startsWith("./")) {
|
||||
// Assume that version numbers are consistent (this should be the case on a Dependabot update PR)
|
||||
actionVersions[actionName] = versionWithComment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return actionVersions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update hardcoded action versions in pr-checks/sync.ts
|
||||
*
|
||||
* @param syncTsPath - Path to sync.ts file
|
||||
* @param actionVersions - Map of action names to versions (may include comments)
|
||||
* @returns True if the file was modified, false otherwise
|
||||
*/
|
||||
export function updateSyncTs(
|
||||
syncTsPath: string,
|
||||
actionVersions: Record<string, string>,
|
||||
): boolean {
|
||||
if (!fs.existsSync(syncTsPath)) {
|
||||
throw new Error(`Could not find ${syncTsPath}`);
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(syncTsPath, "utf8");
|
||||
const originalContent = content;
|
||||
|
||||
// Update hardcoded action versions
|
||||
for (const [actionName, versionWithComment] of Object.entries(
|
||||
actionVersions,
|
||||
)) {
|
||||
// Extract just the version part (before any comment) for sync.ts
|
||||
const version = versionWithComment.includes("#")
|
||||
? versionWithComment.split("#")[0].trim()
|
||||
: versionWithComment.trim();
|
||||
|
||||
// Look for patterns like uses: "actions/setup-node@v4"
|
||||
// Note that this will break if we store an Action uses reference in a
|
||||
// variable - that's a risk we're happy to take since in that case the
|
||||
// PR checks will just fail.
|
||||
const escaped = actionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const pattern = new RegExp(
|
||||
`(uses:\\s*")${escaped}@(?:[^"]+)(")`,
|
||||
"g",
|
||||
);
|
||||
content = content.replace(pattern, `$1${actionName}@${version}$2`);
|
||||
}
|
||||
|
||||
if (content !== originalContent) {
|
||||
fs.writeFileSync(syncTsPath, content, "utf8");
|
||||
console.info(`Updated ${syncTsPath}`);
|
||||
return true;
|
||||
} else {
|
||||
console.info(`No changes needed in ${syncTsPath}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update action versions in template files in pr-checks/checks/
|
||||
*
|
||||
* @param checksDir - Path to pr-checks/checks directory
|
||||
* @param actionVersions - Map of action names to versions (may include comments)
|
||||
* @returns List of files that were modified
|
||||
*/
|
||||
export function updateTemplateFiles(
|
||||
checksDir: string,
|
||||
actionVersions: Record<string, string>,
|
||||
): string[] {
|
||||
const modifiedFiles: string[] = [];
|
||||
|
||||
const templateFiles = fs
|
||||
.readdirSync(checksDir)
|
||||
.filter((f) => f.endsWith(".yml"))
|
||||
.map((f) => path.join(checksDir, f));
|
||||
|
||||
for (const filePath of templateFiles) {
|
||||
let content = fs.readFileSync(filePath, "utf8");
|
||||
const originalContent = content;
|
||||
|
||||
// Update action versions
|
||||
for (const [actionName, versionWithComment] of Object.entries(
|
||||
actionVersions,
|
||||
)) {
|
||||
// Look for patterns like 'uses: actions/setup-node@v4' or 'uses: actions/setup-node@sha # comment'
|
||||
const escaped = actionName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const pattern = new RegExp(
|
||||
`(uses:\\s+${escaped})@(?:[^@\n]+)`,
|
||||
"g",
|
||||
);
|
||||
content = content.replace(pattern, `$1@${versionWithComment}`);
|
||||
}
|
||||
|
||||
if (content !== originalContent) {
|
||||
fs.writeFileSync(filePath, content, "utf8");
|
||||
modifiedFiles.push(filePath);
|
||||
console.info(`Updated ${filePath}`);
|
||||
}
|
||||
}
|
||||
|
||||
return modifiedFiles;
|
||||
}
|
||||
|
||||
function main(): number {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
verbose: {
|
||||
type: "boolean",
|
||||
short: "v",
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
strict: true,
|
||||
});
|
||||
|
||||
const verbose = values.verbose ?? false;
|
||||
|
||||
console.info("Scanning generated workflows for latest action versions...");
|
||||
const actionVersions = scanGeneratedWorkflows(WORKFLOW_DIR);
|
||||
|
||||
if (verbose) {
|
||||
console.info("Found action versions:");
|
||||
for (const [action, version] of Object.entries(actionVersions)) {
|
||||
console.info(` ${action}@${version}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(actionVersions).length === 0) {
|
||||
console.error("No action versions found in generated workflows");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Update files
|
||||
console.info("\nUpdating source files...");
|
||||
const modifiedFiles: string[] = [];
|
||||
|
||||
// Update sync.ts
|
||||
if (updateSyncTs(SYNC_TS_PATH, actionVersions)) {
|
||||
modifiedFiles.push(SYNC_TS_PATH);
|
||||
}
|
||||
|
||||
// Update template files
|
||||
const templateModified = updateTemplateFiles(CHECKS_DIR, actionVersions);
|
||||
modifiedFiles.push(...templateModified);
|
||||
|
||||
if (modifiedFiles.length > 0) {
|
||||
console.info(`\nSync completed. Modified ${modifiedFiles.length} files:`);
|
||||
for (const filePath of modifiedFiles) {
|
||||
console.info(` ${filePath}`);
|
||||
}
|
||||
} else {
|
||||
console.info(
|
||||
"\nNo files needed updating - all action versions are already in sync",
|
||||
);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Only call `main` if this script was run directly.
|
||||
if (require.main === module) {
|
||||
process.exit(main());
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for the sync_back.py script
|
||||
"""
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import sync_back
|
||||
|
||||
|
||||
class TestSyncBack(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Set up temporary directories and files for testing"""
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
self.workflow_dir = os.path.join(self.test_dir, ".github", "workflows")
|
||||
self.checks_dir = os.path.join(self.test_dir, "pr-checks", "checks")
|
||||
os.makedirs(self.workflow_dir)
|
||||
os.makedirs(self.checks_dir)
|
||||
|
||||
# Create sync.py file
|
||||
self.sync_py_path = os.path.join(self.test_dir, "pr-checks", "sync.py")
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up temporary directories"""
|
||||
shutil.rmtree(self.test_dir)
|
||||
|
||||
def test_scan_generated_workflows_basic(self):
|
||||
"""Test basic workflow scanning functionality"""
|
||||
# Create a test generated workflow file
|
||||
workflow_content = """
|
||||
name: Test Workflow
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v5
|
||||
- uses: actions/setup-go@v6
|
||||
"""
|
||||
|
||||
with open(os.path.join(self.workflow_dir, "__test.yml"), 'w') as f:
|
||||
f.write(workflow_content)
|
||||
|
||||
result = sync_back.scan_generated_workflows(self.workflow_dir)
|
||||
|
||||
self.assertEqual(result['actions/checkout'], 'v4')
|
||||
self.assertEqual(result['actions/setup-node'], 'v5')
|
||||
self.assertEqual(result['actions/setup-go'], 'v6')
|
||||
|
||||
def test_scan_generated_workflows_with_comments(self):
|
||||
"""Test scanning workflows with version comments"""
|
||||
workflow_content = """
|
||||
name: Test Workflow
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
- uses: actions/setup-python@v6 # Latest Python
|
||||
"""
|
||||
|
||||
with open(os.path.join(self.workflow_dir, "__test.yml"), 'w') as f:
|
||||
f.write(workflow_content)
|
||||
|
||||
result = sync_back.scan_generated_workflows(self.workflow_dir)
|
||||
|
||||
self.assertEqual(result['actions/checkout'], 'v4')
|
||||
self.assertEqual(result['ruby/setup-ruby'], '44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0')
|
||||
self.assertEqual(result['actions/setup-python'], 'v6 # Latest Python')
|
||||
|
||||
def test_scan_generated_workflows_ignores_local_actions(self):
|
||||
"""Test that local actions (starting with ./) are ignored"""
|
||||
workflow_content = """
|
||||
name: Test Workflow
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: ./.github/actions/local-action
|
||||
- uses: ./another-local-action@v1
|
||||
"""
|
||||
|
||||
with open(os.path.join(self.workflow_dir, "__test.yml"), 'w') as f:
|
||||
f.write(workflow_content)
|
||||
|
||||
result = sync_back.scan_generated_workflows(self.workflow_dir)
|
||||
|
||||
self.assertEqual(result['actions/checkout'], 'v4')
|
||||
self.assertNotIn('./.github/actions/local-action', result)
|
||||
self.assertNotIn('./another-local-action', result)
|
||||
|
||||
|
||||
def test_update_sync_py(self):
|
||||
"""Test updating sync.py file"""
|
||||
sync_py_content = """
|
||||
steps = [
|
||||
{
|
||||
'uses': 'actions/setup-node@v4',
|
||||
'with': {'node-version': '16'}
|
||||
},
|
||||
{
|
||||
'uses': 'actions/setup-go@v5',
|
||||
'with': {'go-version': '1.19'}
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
with open(self.sync_py_path, 'w') as f:
|
||||
f.write(sync_py_content)
|
||||
|
||||
action_versions = {
|
||||
'actions/setup-node': 'v5',
|
||||
'actions/setup-go': 'v6'
|
||||
}
|
||||
|
||||
result = sync_back.update_sync_py(self.sync_py_path, action_versions)
|
||||
self.assertTrue(result)
|
||||
|
||||
with open(self.sync_py_path, 'r') as f:
|
||||
updated_content = f.read()
|
||||
|
||||
self.assertIn("'uses': 'actions/setup-node@v5'", updated_content)
|
||||
self.assertIn("'uses': 'actions/setup-go@v6'", updated_content)
|
||||
|
||||
def test_update_sync_py_with_comments(self):
|
||||
"""Test updating sync.py file when versions have comments"""
|
||||
sync_py_content = """
|
||||
steps = [
|
||||
{
|
||||
'uses': 'actions/setup-node@v4',
|
||||
'with': {'node-version': '16'}
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
with open(self.sync_py_path, 'w') as f:
|
||||
f.write(sync_py_content)
|
||||
|
||||
action_versions = {
|
||||
'actions/setup-node': 'v5 # Latest version'
|
||||
}
|
||||
|
||||
result = sync_back.update_sync_py(self.sync_py_path, action_versions)
|
||||
self.assertTrue(result)
|
||||
|
||||
with open(self.sync_py_path, 'r') as f:
|
||||
updated_content = f.read()
|
||||
|
||||
# sync.py should get the version without comment
|
||||
self.assertIn("'uses': 'actions/setup-node@v5'", updated_content)
|
||||
self.assertNotIn("# Latest version", updated_content)
|
||||
|
||||
def test_update_template_files(self):
|
||||
"""Test updating template files"""
|
||||
template_content = """
|
||||
name: Test Template
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
"""
|
||||
|
||||
template_path = os.path.join(self.checks_dir, "test.yml")
|
||||
with open(template_path, 'w') as f:
|
||||
f.write(template_content)
|
||||
|
||||
action_versions = {
|
||||
'actions/checkout': 'v4',
|
||||
'actions/setup-node': 'v5 # Latest'
|
||||
}
|
||||
|
||||
result = sync_back.update_template_files(self.checks_dir, action_versions)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertIn(template_path, result)
|
||||
|
||||
with open(template_path, 'r') as f:
|
||||
updated_content = f.read()
|
||||
|
||||
self.assertIn("uses: actions/checkout@v4", updated_content)
|
||||
self.assertIn("uses: actions/setup-node@v5 # Latest", updated_content)
|
||||
|
||||
def test_update_template_files_preserves_comments(self):
|
||||
"""Test that updating template files preserves version comments"""
|
||||
template_content = """
|
||||
name: Test Template
|
||||
steps:
|
||||
- uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.256.0
|
||||
"""
|
||||
|
||||
template_path = os.path.join(self.checks_dir, "test.yml")
|
||||
with open(template_path, 'w') as f:
|
||||
f.write(template_content)
|
||||
|
||||
action_versions = {
|
||||
'ruby/setup-ruby': '55511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0'
|
||||
}
|
||||
|
||||
result = sync_back.update_template_files(self.checks_dir, action_versions)
|
||||
self.assertEqual(len(result), 1)
|
||||
|
||||
with open(template_path, 'r') as f:
|
||||
updated_content = f.read()
|
||||
|
||||
self.assertIn("uses: ruby/setup-ruby@55511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0", updated_content)
|
||||
|
||||
def test_no_changes_needed(self):
|
||||
"""Test that functions return False/empty when no changes are needed"""
|
||||
# Test sync.py with no changes needed
|
||||
sync_py_content = """
|
||||
steps = [
|
||||
{
|
||||
'uses': 'actions/setup-node@v5',
|
||||
'with': {'node-version': '16'}
|
||||
}
|
||||
]
|
||||
"""
|
||||
|
||||
with open(self.sync_py_path, 'w') as f:
|
||||
f.write(sync_py_content)
|
||||
|
||||
action_versions = {
|
||||
'actions/setup-node': 'v5'
|
||||
}
|
||||
|
||||
result = sync_back.update_sync_py(self.sync_py_path, action_versions)
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user