Files
Jonathan Miller 66e728b078
Generic: Repo Health / Access control (push) Successful in 18s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: Auto Version Bump / Version Bump (push) Failing after 27s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 28s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 1m7s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 1m7s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Generic: Repo Health / Release configuration (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Repo Health / Release configuration (push) Has been cancelled
Generic: Repo Health / Scripts governance (push) Has been cancelled
Generic: Repo Health / Repository health (push) Has been cancelled
style: fix PHPCS violations across migrated CLI scripts
Auto-fixed 5006 tab-indent and line-ending errors via phpcbf, then
manually broke 100 lines exceeding 150-char limit. All 74 files in
cli/, automation/, maintenance/, deploy/ now pass PHPCS PSR-12 clean.

Authored-by: Moko Consulting
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-31 13:36:05 -05:00

237 lines
7.0 KiB
PHP

#!/usr/bin/env php
<?php
/**
* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
*
* This file is part of a Moko Consulting project.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*
* FILE INFORMATION
* DEFGROUP: MokoPlatform.Scripts.Maintenance
* INGROUP: MokoPlatform
* REPO: https://git.mokoconsulting.tech/MokoConsulting/moko-platform
* PATH: /maintenance/pin_action_shas.php
* BRIEF: Pin GitHub Actions to immutable commit SHAs in workflow files
* NOTE: Resolves tag/branch refs to commit SHAs via the GitHub API to satisfy
* the CodeQL "Unpinned tag for a non-immutable Action" security rule.
*/
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../lib/Enterprise/CliFramework.php';
use MokoEnterprise\CliFramework;
use MokoEnterprise\Config;
use MokoEnterprise\GitPlatformAdapter;
use MokoEnterprise\PlatformAdapterFactory;
class PinActionShasCli extends CliFramework
{
private ?GitPlatformAdapter $adapter = null;
private string $workflowsDir = '.github/workflows';
/** @var array<string, string> resolved-ref -> SHA cache */
private array $shaCache = [];
/** @var list<array{file:string,line:int,old:string,new:string}> */
private array $changes = [];
protected function configure(): void
{
$this->setDescription('Pin GitHub Actions to immutable commit SHAs in workflow files');
}
protected function initialize(): void
{
$config = Config::load();
try {
$this->adapter = PlatformAdapterFactory::create($config);
$this->workflowsDir = $this->adapter->getWorkflowDir();
} catch (\RuntimeException $e) {
$this->log('WARNING', $e->getMessage() . " — falling back to unauthenticated mode");
}
}
protected function run(): int
{
$this->log('INFO', "GitHub Actions SHA Pinner");
$this->log('INFO', str_repeat('=', 50));
if ($this->dryRun) {
$this->log('INFO', "Mode: DRY RUN (no files will be modified)\n");
}
// Also check .github/workflows if on Gitea (workflows may exist in both dirs)
$dirs = [$this->workflowsDir];
if ($this->workflowsDir !== '.github/workflows' && is_dir('.github/workflows')) {
$dirs[] = '.github/workflows';
}
$files = [];
foreach ($dirs as $dir) {
$files = array_merge($files, glob($dir . '/*.yml') ?: []);
}
if (empty($files)) {
$this->log('INFO', 'No workflow files found in ' . $this->workflowsDir);
return 0;
}
$this->log('INFO', 'Found ' . count($files) . " workflow file(s)\n");
foreach ($files as $file) {
$this->processFile($file);
}
$this->printChangeSummary(count($files));
return 0;
}
private function processFile(string $file): void
{
$content = file_get_contents($file);
if ($content === false) {
$this->log('ERROR', "Cannot read: {$file}");
return;
}
$lines = explode("\n", $content);
$modified = false;
foreach ($lines as $idx => &$line) {
$updated = $this->processLine($line, $file, $idx + 1);
if ($updated !== null) {
$this->changes[] = [
'file' => $file,
'line' => $idx + 1,
'old' => trim($line),
'new' => trim($updated),
];
$line = $updated;
$modified = true;
}
}
unset($line);
if ($modified) {
if (!$this->dryRun) {
if (file_put_contents($file, implode("\n", $lines)) === false) {
$this->log('ERROR', "Cannot write: {$file}");
return;
}
}
$this->log('INFO', ($this->dryRun ? '(dry-run) ' : '') . "Updated: {$file}");
} elseif ($this->verbose) {
$this->log('INFO', "No changes: {$file}");
}
}
/**
* Inspect one line and return the pinned replacement, or null if the line
* does not need to be changed.
*/
private function processLine(string $line, string $file, int $lineNum): ?string
{
if (
!preg_match(
'/^(\s+uses:\s+)([\w.\-]+\/[\w.\-\/]+)@([^\s#]+)((?:\s+#.*)?)$/',
$line,
$m
)
) {
return null;
}
[, $prefix, $action, $ref, $trailingComment] = $m;
// Already pinned
if (preg_match('/^[0-9a-f]{40}$/', $ref)) {
if ($this->verbose) {
$this->log('INFO', " Already pinned: {$action}@{$ref}");
}
return null;
}
$segments = explode('/', $action);
if (count($segments) < 2) {
return null;
}
[$owner, $repo] = $segments;
$sha = $this->resolveTagToSha($owner, $repo, $ref);
if ($sha === null) {
$this->log('WARNING', "Cannot resolve {$action}@{$ref} ({$file}:{$lineNum}) — skipping");
return null;
}
return "{$prefix}{$action}@{$sha} # {$ref}";
}
private function resolveTagToSha(string $owner, string $repo, string $ref): ?string
{
$cacheKey = "{$owner}/{$repo}@{$ref}";
if (array_key_exists($cacheKey, $this->shaCache)) {
return $this->shaCache[$cacheKey];
}
if ($this->verbose) {
$this->log('INFO', " Resolving {$owner}/{$repo}@{$ref} ...");
}
$sha = null;
if ($this->adapter !== null) {
try {
$sha = $this->adapter->resolveRef($owner, $repo, $ref);
if (empty($sha)) {
$sha = null;
}
} catch (\Exception $e) {
if ($this->verbose) {
$this->log('WARNING', " adapter resolve failed: " . $e->getMessage());
}
$this->adapter->getApiClient()->resetCircuitBreaker();
}
}
if ($sha !== null && $this->verbose) {
$this->log('INFO', " -> {$sha}");
}
$this->shaCache[$cacheKey] = $sha;
return $sha;
}
private function printChangeSummary(int $fileCount): void
{
$this->log('INFO', "\nSummary:");
$this->log('INFO', " Files scanned: {$fileCount}");
$this->log('INFO', " Actions pinned: " . count($this->changes));
if (!empty($this->changes)) {
$this->log('INFO', "\nChanges made:");
foreach ($this->changes as $change) {
$this->log('INFO', " {$change['file']}:{$change['line']}");
$this->log('INFO', " - {$change['old']}");
$this->log('INFO', " + {$change['new']}");
}
} else {
$this->log('INFO', "\nAll actions are already pinned to commit SHAs");
}
}
}
$app = new PinActionShasCli();
exit($app->execute());