ci(workflows): consolidate policy and rust workflow setup (#564)
* fix(workflows): standardize runner configuration for security jobs * ci(actionlint): add Blacksmith runner label to config Add blacksmith-2vcpu-ubuntu-2404 to actionlint self-hosted-runner labels config to suppress "unknown label" warnings during workflow linting. This label is used across all workflows after the Blacksmith migration. * fix(actionlint): adjust indentation for self-hosted runner labels * feat(security): enhance security workflow with CodeQL analysis steps * fix(security): update CodeQL action to version 4 for improved analysis * fix(security): remove duplicate permissions in security workflow * fix(security): revert CodeQL action to v3 for stability The v4 version was causing workflow file validation failures. Reverting to proven v3 version that is working on main branch. * fix(security): remove duplicate permissions causing workflow validation failure The permissions block had duplicate security-events and actions keys, which caused YAML validation errors and prevented workflow execution. Fixes: workflow file validation failures on main branch * fix(security): remove pull_request trigger to reduce costs * fix(security): restore PR trigger but skip codeql on PRs * fix(security): resolve YAML syntax error in security workflow * refactor(security): split CodeQL into dedicated scheduled workflow * fix(security): update workflow name to Rust Package Security Audit * fix(codeql): remove push trigger, keep schedule and on-demand only * feat(codeql): add CodeQL configuration file to ignore specific paths * Potential fix for code scanning alert no. 39: Hard-coded cryptographic value Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * fix(ci): resolve auto-response workflow merge markers * fix(build): restore ChannelMessage reply_target usage * ci(workflows): run workflow sanity on workflow pushes for all branches * ci(workflows): rename auto-response workflow to PR Auto Responder * ci(workflows): require owner approval for workflow file changes * ci: add lint-first PR feedback gate * ci(workflows): split label policy checks from workflow sanity * ci(workflows): consolidate policy and rust workflow setup --------- Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
This commit is contained in:
parent
0f68756ec7
commit
32bfe1d186
10 changed files with 206 additions and 58 deletions
21
.github/label-policy.json
vendored
Normal file
21
.github/label-policy.json
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"contributor_tier_color": "2ED9FF",
|
||||
"contributor_tiers": [
|
||||
{
|
||||
"label": "distinguished contributor",
|
||||
"min_merged_prs": 50
|
||||
},
|
||||
{
|
||||
"label": "principal contributor",
|
||||
"min_merged_prs": 20
|
||||
},
|
||||
{
|
||||
"label": "experienced contributor",
|
||||
"min_merged_prs": 10
|
||||
},
|
||||
{
|
||||
"label": "trusted contributor",
|
||||
"min_merged_prs": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
41
.github/workflows/auto-response.yml
vendored
41
.github/workflows/auto-response.yml
vendored
|
|
@ -29,14 +29,41 @@ jobs:
|
|||
const issue = context.payload.issue;
|
||||
const pullRequest = context.payload.pull_request;
|
||||
const target = issue ?? pullRequest;
|
||||
const contributorTierRules = [
|
||||
{ label: "distinguished contributor", minMergedPRs: 50 },
|
||||
{ label: "principal contributor", minMergedPRs: 20 },
|
||||
{ label: "experienced contributor", minMergedPRs: 10 },
|
||||
{ label: "trusted contributor", minMergedPRs: 5 },
|
||||
];
|
||||
async function loadContributorTierPolicy() {
|
||||
const fallback = {
|
||||
contributorTierColor: "2ED9FF",
|
||||
contributorTierRules: [
|
||||
{ label: "distinguished contributor", minMergedPRs: 50 },
|
||||
{ label: "principal contributor", minMergedPRs: 20 },
|
||||
{ label: "experienced contributor", minMergedPRs: 10 },
|
||||
{ label: "trusted contributor", minMergedPRs: 5 },
|
||||
],
|
||||
};
|
||||
try {
|
||||
const { data } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: ".github/label-policy.json",
|
||||
ref: context.payload.repository?.default_branch || "main",
|
||||
});
|
||||
const json = JSON.parse(Buffer.from(data.content, "base64").toString("utf8"));
|
||||
const contributorTierRules = (json.contributor_tiers || []).map((entry) => ({
|
||||
label: String(entry.label || "").trim(),
|
||||
minMergedPRs: Number(entry.min_merged_prs || 0),
|
||||
}));
|
||||
const contributorTierColor = String(json.contributor_tier_color || "").toUpperCase();
|
||||
if (!contributorTierColor || contributorTierRules.length === 0) {
|
||||
return fallback;
|
||||
}
|
||||
return { contributorTierColor, contributorTierRules };
|
||||
} catch (error) {
|
||||
core.warning(`failed to load .github/label-policy.json, using fallback policy: ${error.message}`);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
const { contributorTierColor, contributorTierRules } = await loadContributorTierPolicy();
|
||||
const contributorTierLabels = contributorTierRules.map((rule) => rule.label);
|
||||
const contributorTierColor = "2ED9FF"; // Keep in sync with .github/workflows/labeler.yml
|
||||
const managedContributorLabels = new Set(contributorTierLabels);
|
||||
const action = context.payload.action;
|
||||
const changedLabel = context.payload.label?.name;
|
||||
|
|
|
|||
59
.github/workflows/label-policy-sanity.yml
vendored
59
.github/workflows/label-policy-sanity.yml
vendored
|
|
@ -3,10 +3,12 @@ name: Label Policy Sanity
|
|||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/label-policy.json"
|
||||
- ".github/workflows/labeler.yml"
|
||||
- ".github/workflows/auto-response.yml"
|
||||
push:
|
||||
paths:
|
||||
- ".github/label-policy.json"
|
||||
- ".github/workflows/labeler.yml"
|
||||
- ".github/workflows/auto-response.yml"
|
||||
|
||||
|
|
@ -25,39 +27,48 @@ jobs:
|
|||
- name: Checkout
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Verify contributor-tier parity across workflows
|
||||
- name: Verify shared label policy and workflow wiring
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
files = [
|
||||
policy_path = Path('.github/label-policy.json')
|
||||
policy = json.loads(policy_path.read_text(encoding='utf-8'))
|
||||
color = str(policy.get('contributor_tier_color', '')).upper()
|
||||
rules = policy.get('contributor_tiers', [])
|
||||
if not re.fullmatch(r'[0-9A-F]{6}', color):
|
||||
raise SystemExit('invalid contributor_tier_color in .github/label-policy.json')
|
||||
if not rules:
|
||||
raise SystemExit('contributor_tiers must not be empty in .github/label-policy.json')
|
||||
|
||||
labels = set()
|
||||
prev_min = None
|
||||
for entry in rules:
|
||||
label = str(entry.get('label', '')).strip().lower()
|
||||
min_merged = int(entry.get('min_merged_prs', 0))
|
||||
if not label.endswith('contributor'):
|
||||
raise SystemExit(f'invalid contributor tier label: {label}')
|
||||
if label in labels:
|
||||
raise SystemExit(f'duplicate contributor tier label: {label}')
|
||||
if prev_min is not None and min_merged > prev_min:
|
||||
raise SystemExit('contributor_tiers must be sorted descending by min_merged_prs')
|
||||
labels.add(label)
|
||||
prev_min = min_merged
|
||||
|
||||
workflow_paths = [
|
||||
Path('.github/workflows/labeler.yml'),
|
||||
Path('.github/workflows/auto-response.yml'),
|
||||
]
|
||||
for workflow in workflow_paths:
|
||||
text = workflow.read_text(encoding='utf-8')
|
||||
if '.github/label-policy.json' not in text:
|
||||
raise SystemExit(f'{workflow} must load .github/label-policy.json')
|
||||
if re.search(r'contributorTierColor\s*=\s*"[0-9A-Fa-f]{6}"', text):
|
||||
raise SystemExit(f'{workflow} contains hardcoded contributorTierColor')
|
||||
|
||||
parsed = {}
|
||||
for path in files:
|
||||
text = path.read_text(encoding='utf-8')
|
||||
rules = re.findall(r'\{ label: "([^"]+ contributor)", minMergedPRs: (\d+) \}', text)
|
||||
color_match = re.search(r'const contributorTierColor = "([0-9A-Fa-f]{6})"', text)
|
||||
if not color_match:
|
||||
raise SystemExit(f'failed to parse contributorTierColor in {path}')
|
||||
parsed[str(path)] = {
|
||||
'rules': rules,
|
||||
'color': color_match.group(1).upper(),
|
||||
}
|
||||
|
||||
baseline = parsed[str(files[0])]
|
||||
for path in files[1:]:
|
||||
entry = parsed[str(path)]
|
||||
if entry != baseline:
|
||||
raise SystemExit(
|
||||
'contributor-tier mismatch between workflows: '
|
||||
f'{files[0]}={baseline} vs {path}={entry}'
|
||||
)
|
||||
|
||||
print('contributor tier rules/color are consistent across label workflows')
|
||||
print('label policy file is valid and workflow consumers are wired to shared policy')
|
||||
PY
|
||||
|
|
|
|||
41
.github/workflows/labeler.yml
vendored
41
.github/workflows/labeler.yml
vendored
|
|
@ -60,14 +60,41 @@ jobs:
|
|||
return;
|
||||
}
|
||||
|
||||
const contributorTierRules = [
|
||||
{ label: "distinguished contributor", minMergedPRs: 50 },
|
||||
{ label: "principal contributor", minMergedPRs: 20 },
|
||||
{ label: "experienced contributor", minMergedPRs: 10 },
|
||||
{ label: "trusted contributor", minMergedPRs: 5 },
|
||||
];
|
||||
async function loadContributorTierPolicy() {
|
||||
const fallback = {
|
||||
contributorTierColor: "2ED9FF",
|
||||
contributorTierRules: [
|
||||
{ label: "distinguished contributor", minMergedPRs: 50 },
|
||||
{ label: "principal contributor", minMergedPRs: 20 },
|
||||
{ label: "experienced contributor", minMergedPRs: 10 },
|
||||
{ label: "trusted contributor", minMergedPRs: 5 },
|
||||
],
|
||||
};
|
||||
try {
|
||||
const { data } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: ".github/label-policy.json",
|
||||
ref: context.payload.repository?.default_branch || "main",
|
||||
});
|
||||
const json = JSON.parse(Buffer.from(data.content, "base64").toString("utf8"));
|
||||
const contributorTierRules = (json.contributor_tiers || []).map((entry) => ({
|
||||
label: String(entry.label || "").trim(),
|
||||
minMergedPRs: Number(entry.min_merged_prs || 0),
|
||||
}));
|
||||
const contributorTierColor = String(json.contributor_tier_color || "").toUpperCase();
|
||||
if (!contributorTierColor || contributorTierRules.length === 0) {
|
||||
return fallback;
|
||||
}
|
||||
return { contributorTierColor, contributorTierRules };
|
||||
} catch (error) {
|
||||
core.warning(`failed to load .github/label-policy.json, using fallback policy: ${error.message}`);
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
const { contributorTierColor, contributorTierRules } = await loadContributorTierPolicy();
|
||||
const contributorTierLabels = contributorTierRules.map((rule) => rule.label);
|
||||
const contributorTierColor = "2ED9FF"; // Keep in sync with .github/workflows/auto-response.yml
|
||||
|
||||
const managedPathLabels = [
|
||||
"docs",
|
||||
|
|
|
|||
2
.github/workflows/pr-hygiene.yml
vendored
2
.github/workflows/pr-hygiene.yml
vendored
|
|
@ -26,7 +26,7 @@ jobs:
|
|||
with:
|
||||
script: |
|
||||
const staleHours = Number(process.env.STALE_HOURS || "48");
|
||||
const ignoreLabels = new Set(["no-stale", "maintainer", "no-pr-hygiene"]);
|
||||
const ignoreLabels = new Set(["no-stale", "stale", "maintainer", "no-pr-hygiene"]);
|
||||
const marker = "<!-- pr-hygiene-nudge -->";
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
|
|
|
|||
62
.github/workflows/rust-reusable.yml
vendored
Normal file
62
.github/workflows/rust-reusable.yml
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
name: Rust Reusable Job
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
run_command:
|
||||
description: "Shell command(s) to execute."
|
||||
required: true
|
||||
type: string
|
||||
timeout_minutes:
|
||||
description: "Job timeout in minutes."
|
||||
required: false
|
||||
default: 20
|
||||
type: number
|
||||
toolchain:
|
||||
description: "Rust toolchain channel/version."
|
||||
required: false
|
||||
default: "stable"
|
||||
type: string
|
||||
components:
|
||||
description: "Optional rustup components."
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
targets:
|
||||
description: "Optional rustup targets."
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
use_cache:
|
||||
description: "Whether to enable rust-cache."
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
run:
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: ${{ inputs.timeout_minutes }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Setup Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
with:
|
||||
toolchain: ${{ inputs.toolchain }}
|
||||
components: ${{ inputs.components }}
|
||||
targets: ${{ inputs.targets }}
|
||||
|
||||
- name: Restore Rust cache
|
||||
if: inputs.use_cache
|
||||
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
|
||||
- name: Run command
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
${{ inputs.run_command }}
|
||||
20
.github/workflows/security.yml
vendored
20
.github/workflows/security.yml
vendored
|
|
@ -23,19 +23,13 @@ env:
|
|||
jobs:
|
||||
audit:
|
||||
name: Security Audit
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
||||
|
||||
- name: Install cargo-audit
|
||||
run: cargo install --locked cargo-audit --version 0.22.1
|
||||
|
||||
- name: Run cargo-audit
|
||||
run: cargo audit
|
||||
uses: ./.github/workflows/rust-reusable.yml
|
||||
with:
|
||||
timeout_minutes: 20
|
||||
toolchain: stable
|
||||
run_command: |
|
||||
cargo install --locked cargo-audit --version 0.22.1
|
||||
cargo audit
|
||||
|
||||
deny:
|
||||
name: License & Supply Chain
|
||||
|
|
|
|||
4
.github/workflows/stale.yml
vendored
4
.github/workflows/stale.yml
vendored
|
|
@ -24,8 +24,8 @@ jobs:
|
|||
days-before-pr-close: 7
|
||||
stale-issue-label: stale
|
||||
stale-pr-label: stale
|
||||
exempt-issue-labels: security,pinned,no-stale,maintainer
|
||||
exempt-pr-labels: no-stale,maintainer
|
||||
exempt-issue-labels: security,pinned,no-stale,no-pr-hygiene,maintainer
|
||||
exempt-pr-labels: no-stale,no-pr-hygiene,maintainer
|
||||
remove-stale-when-updated: true
|
||||
exempt-all-assignees: true
|
||||
operations-per-run: 300
|
||||
|
|
|
|||
8
.github/workflows/update-notice.yml
vendored
8
.github/workflows/update-notice.yml
vendored
|
|
@ -6,6 +6,10 @@ on:
|
|||
# Run every Sunday at 00:00 UTC
|
||||
- cron: '0 0 * * 0'
|
||||
|
||||
concurrency:
|
||||
group: update-notice-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
|
@ -13,10 +17,10 @@ permissions:
|
|||
jobs:
|
||||
update-notice:
|
||||
name: Update NOTICE with new contributors
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
|
||||
- name: Fetch contributors
|
||||
id: contributors
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
|
|||
- `.github/workflows/release.yml` (`Release`)
|
||||
- Purpose: build tagged release artifacts and publish GitHub releases
|
||||
- `.github/workflows/label-policy-sanity.yml` (`Label Policy Sanity`)
|
||||
- Purpose: enforce contributor-tier rule/color parity between `labeler.yml` and `auto-response.yml`
|
||||
- Purpose: validate shared contributor-tier policy in `.github/label-policy.json` and ensure label workflows consume that policy
|
||||
- `.github/workflows/rust-reusable.yml` (`Rust Reusable Job`)
|
||||
- Purpose: reusable Rust setup/cache + command runner for workflow-call consumers
|
||||
|
||||
### Optional Repository Automation
|
||||
|
||||
|
|
@ -62,7 +64,7 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
|
|||
- `Release`: tag push (`v*`)
|
||||
- `Security Audit`: push to `main`, PRs to `main`, weekly schedule
|
||||
- `Workflow Sanity`: PR/push when `.github/workflows/**`, `.github/*.yml`, or `.github/*.yaml` change
|
||||
- `Label Policy Sanity`: PR/push when `.github/workflows/labeler.yml` or `.github/workflows/auto-response.yml` changes
|
||||
- `Label Policy Sanity`: PR/push when `.github/label-policy.json`, `.github/workflows/labeler.yml`, or `.github/workflows/auto-response.yml` changes
|
||||
- `PR Labeler`: `pull_request_target` lifecycle events
|
||||
- `PR Auto Responder`: issue opened/labeled, `pull_request_target` opened/labeled
|
||||
- `Stale`: daily schedule, manual dispatch
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue