Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .claude/commands/repo-maintenance.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
description: Comprehensive repository maintenance - run all health checks and updates
allowed-tools: Read, Bash(script/repo-maintenance.sh:*), Bash(git:*), Bash(gh:*), Bash(npm:*), Bash(pnpm:*), Bash(jq:*), Skill
argument-hint: '[--mode full|quick|check-only] [--skip CATEGORY] [--create-pr]'
argument-hint: '[--mode full|quick|check-only] [--skip CATEGORY] [--create-pr] [--check-actions-pr-settings]'
---

# Repository Maintenance Workflow
Expand Down Expand Up @@ -29,6 +29,8 @@ script/repo-maintenance.sh $ARGUMENTS
Repository state guard runs before updates. Archived repositories switch to `check-only` and skip PR creation.

- Private repositories allow Dependency Review to be optional or skipped.
- GitHub Actions PR creation settings are checked with `script/repo-maintenance.sh --check-actions-pr-settings`.
- Automated issue and maintenance PR creation expects `default_workflow_permissions=write` and `can_approve_pull_request_reviews=true`.
- Managed workflow templates are checked against `templates/workflows/` with `npm run workflow:sync:check`.
- Workflow Lint coverage checks verify `.github/workflows/`, `.github/workflows/templates/`, and `templates/workflows/` are collected without static unmatched globs.

Expand Down
60 changes: 59 additions & 1 deletion script/repo-maintenance.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ MODE="full"
SKIP_CATEGORIES=""
CREATE_PR=false
CHECK_REQUIRED_WORKFLOWS_ONLY=false
CHECK_ACTIONS_PR_SETTINGS_ONLY=false
CONTEXT_DIR="${CONTEXT_DIR:-.context}"

usage() {
cat <<'EOF'
Usage: script/repo-maintenance.sh [--mode full|quick|check-only] [--skip CATEGORY] [--create-pr] [--check-required-workflows]
Usage: script/repo-maintenance.sh [--mode full|quick|check-only] [--skip CATEGORY] [--create-pr] [--check-required-workflows] [--check-actions-pr-settings]
EOF
}

Expand All @@ -39,6 +40,11 @@ while [[ $# -gt 0 ]]; do
MODE="check-only"
shift
;;
--check-actions-pr-settings)
CHECK_ACTIONS_PR_SETTINGS_ONLY=true
MODE="check-only"
shift
;;
-h|--help)
usage
exit 0
Expand Down Expand Up @@ -173,6 +179,52 @@ check_repository_state() {
fi
}

check_actions_pr_creation_settings() {
local repo settings default_permissions can_create_pr settings_url issue_count=0

if ! command -v gh >/dev/null 2>&1; then
output::warning "GitHub Actions PR creation settings check skipped: gh not found"
return 0
fi
if ! command -v jq >/dev/null 2>&1; then
output::warning "GitHub Actions PR creation settings check skipped: jq not found"
return 0
fi

repo="$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || true)"
if [[ -z "$repo" || "$repo" == "null" ]]; then
output::warning "GitHub Actions PR creation settings check skipped: repository unavailable"
return 0
fi

settings="$(gh api "repos/$repo/actions/permissions/workflow" 2>/dev/null || true)"
if [[ -z "$settings" ]]; then
output::warning "GitHub Actions PR creation settings unavailable for $repo"
return 0
fi

default_permissions="$(echo "$settings" | jq -r '.default_workflow_permissions // empty')"
can_create_pr="$(echo "$settings" | jq -r '.can_approve_pull_request_reviews // false')"
settings_url="https://github.com/$repo/settings/actions"

if [[ "$default_permissions" != "write" ]]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not require write-all default workflow permissions

When a repo keeps the default GITHUB_TOKEN setting at read-only but grants contents: write/pull-requests: write on the specific PR-creating jobs, this check fails even though PR creation is configured correctly; GitHub documents that the workflow/job permissions key modifies the token for that job (https://docs.github.com/en/actions/tutorials/authenticate-with-github_token#modifying-the-permissions-for-the-github_token), and this repo already does that in .github/workflows/update-libraries.yml:23-25, .github/workflows/scheduled-maintenance.yml:31-33, and .github/workflows/claude.yml:51-53. Requiring the repo-wide default to be write makes --check-actions-pr-settings reject the safer read-only-default configuration and tells maintainers to broaden every workflow unnecessarily.

Useful? React with 👍 / 👎.

output::warning "GitHub Actions default workflow permissions are '$default_permissions' (expected: write)"
issue_count=$((issue_count + 1))
fi

if [[ "$can_create_pr" != "true" ]]; then
output::warning "GitHub Actions PR creation is disabled (expected: Allow GitHub Actions to create and approve pull requests)"
issue_count=$((issue_count + 1))
fi

if [[ "$issue_count" -gt 0 ]]; then
echo "Settings: $settings_url"
return 1
fi

output::success "GitHub Actions PR creation settings ok"
}

check_workflow_templates() {
if npm run workflow:sync:check --if-present >/dev/null 2>&1; then
output::success "Workflow template sync ok"
Expand Down Expand Up @@ -334,6 +386,11 @@ if [[ "$CHECK_REQUIRED_WORKFLOWS_ONLY" == "true" ]]; then
exit $?
fi

if [[ "$CHECK_ACTIONS_PR_SETTINGS_ONLY" == "true" ]]; then
check_actions_pr_creation_settings
exit $?
fi

cat <<EOF
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Repository Maintenance
Expand All @@ -347,6 +404,7 @@ EOF
check_repository_state

if ! has_skip "setup"; then
check_actions_pr_creation_settings || true
check_workflow_templates
check_workflow_template_lint_coverage
check_managed_templates
Expand Down
89 changes: 89 additions & 0 deletions test/repo-maintenance-actions-settings.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');

const repoPath = path.resolve(__dirname, '..');

function readRepoFile(relativePath) {
return fs.readFileSync(path.join(repoPath, relativePath), 'utf8');
}

function runActionsPrSettingsScript(workflowSettings) {
const contextDir = path.join(repoPath, '.context');
fs.mkdirSync(contextDir, { recursive: true });
const tempRoot = fs.mkdtempSync(path.join(contextDir, 'actions-pr-settings-test-'));

try {
const binDir = path.join(tempRoot, 'bin');
fs.mkdirSync(binDir, { recursive: true });
const ghPath = path.join(binDir, 'gh');
fs.writeFileSync(ghPath, buildGhStub(workflowSettings));
fs.chmodSync(ghPath, 0o755);

const scriptPath = path.join(repoPath, 'script', 'repo-maintenance.sh');
try {
const stdout = execFileSync('bash', [scriptPath, '--check-actions-pr-settings'], {
cwd: tempRoot,
env: { ...process.env, PATH: `${binDir}:${process.env.PATH}` },
encoding: 'utf8',
});
return { status: 0, output: stdout };
} catch (error) {
return { status: error.status, output: `${error.stdout || ''}${error.stderr || ''}` };
}
} finally {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
}

function buildGhStub(workflowSettings) {
return [
'#!/bin/sh',
'if [ "$1" = "repo" ] && [ "$2" = "view" ]; then',
' echo "owner/repo"',
' exit 0',
'fi',
'if [ "$1" = "api" ]; then',
` cat <<'JSON'\n${JSON.stringify(workflowSettings)}\nJSON`,
' exit 0',
'fi',
'exit 1',
'',
].join('\n');
}

describe('repo-maintenance GitHub Actions PR creation settings', () => {
test('documents and checks repository workflow permissions needed for automated PR creation', () => {
const command = readRepoFile('.claude/commands/repo-maintenance.md');
const script = readRepoFile('script/repo-maintenance.sh');

expect(command).toContain('script/repo-maintenance.sh --check-actions-pr-settings');
expect(command).toContain('default_workflow_permissions=write');
expect(command).toContain('can_approve_pull_request_reviews=true');
expect(script).toContain('check_actions_pr_creation_settings');
expect(script).toContain('repos/$repo/actions/permissions/workflow');
expect(script).toContain('https://github.com/$repo/settings/actions');
});

test('succeeds when Actions can create pull requests', () => {
const result = runActionsPrSettingsScript({
default_workflow_permissions: 'write',
can_approve_pull_request_reviews: true,
});

expect(result.status).toBe(0);
expect(result.output).toContain('GitHub Actions PR creation settings ok');
});

test('warns with settings URL when Actions cannot create pull requests', () => {
const result = runActionsPrSettingsScript({
default_workflow_permissions: 'read',
can_approve_pull_request_reviews: false,
});

expect(result.status).toBe(1);
expect(result.output).toContain("default workflow permissions are 'read'");
expect(result.output).toContain('GitHub Actions PR creation is disabled');
expect(result.output).toContain('https://github.com/owner/repo/settings/actions');
});
});
Loading