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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
33
.github/workflows/auto-response.yml
vendored
33
.github/workflows/auto-response.yml
vendored
|
|
@ -29,14 +29,41 @@ jobs:
|
||||||
const issue = context.payload.issue;
|
const issue = context.payload.issue;
|
||||||
const pullRequest = context.payload.pull_request;
|
const pullRequest = context.payload.pull_request;
|
||||||
const target = issue ?? pullRequest;
|
const target = issue ?? pullRequest;
|
||||||
const contributorTierRules = [
|
async function loadContributorTierPolicy() {
|
||||||
|
const fallback = {
|
||||||
|
contributorTierColor: "2ED9FF",
|
||||||
|
contributorTierRules: [
|
||||||
{ label: "distinguished contributor", minMergedPRs: 50 },
|
{ label: "distinguished contributor", minMergedPRs: 50 },
|
||||||
{ label: "principal contributor", minMergedPRs: 20 },
|
{ label: "principal contributor", minMergedPRs: 20 },
|
||||||
{ label: "experienced contributor", minMergedPRs: 10 },
|
{ label: "experienced contributor", minMergedPRs: 10 },
|
||||||
{ label: "trusted contributor", minMergedPRs: 5 },
|
{ 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 contributorTierLabels = contributorTierRules.map((rule) => rule.label);
|
||||||
const contributorTierColor = "2ED9FF"; // Keep in sync with .github/workflows/labeler.yml
|
|
||||||
const managedContributorLabels = new Set(contributorTierLabels);
|
const managedContributorLabels = new Set(contributorTierLabels);
|
||||||
const action = context.payload.action;
|
const action = context.payload.action;
|
||||||
const changedLabel = context.payload.label?.name;
|
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:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
|
- ".github/label-policy.json"
|
||||||
- ".github/workflows/labeler.yml"
|
- ".github/workflows/labeler.yml"
|
||||||
- ".github/workflows/auto-response.yml"
|
- ".github/workflows/auto-response.yml"
|
||||||
push:
|
push:
|
||||||
paths:
|
paths:
|
||||||
|
- ".github/label-policy.json"
|
||||||
- ".github/workflows/labeler.yml"
|
- ".github/workflows/labeler.yml"
|
||||||
- ".github/workflows/auto-response.yml"
|
- ".github/workflows/auto-response.yml"
|
||||||
|
|
||||||
|
|
@ -25,39 +27,48 @@ jobs:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Verify contributor-tier parity across workflows
|
- name: Verify shared label policy and workflow wiring
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
python3 - <<'PY'
|
python3 - <<'PY'
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
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/labeler.yml'),
|
||||||
Path('.github/workflows/auto-response.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 = {}
|
print('label policy file is valid and workflow consumers are wired to shared policy')
|
||||||
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')
|
|
||||||
PY
|
PY
|
||||||
|
|
|
||||||
33
.github/workflows/labeler.yml
vendored
33
.github/workflows/labeler.yml
vendored
|
|
@ -60,14 +60,41 @@ jobs:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const contributorTierRules = [
|
async function loadContributorTierPolicy() {
|
||||||
|
const fallback = {
|
||||||
|
contributorTierColor: "2ED9FF",
|
||||||
|
contributorTierRules: [
|
||||||
{ label: "distinguished contributor", minMergedPRs: 50 },
|
{ label: "distinguished contributor", minMergedPRs: 50 },
|
||||||
{ label: "principal contributor", minMergedPRs: 20 },
|
{ label: "principal contributor", minMergedPRs: 20 },
|
||||||
{ label: "experienced contributor", minMergedPRs: 10 },
|
{ label: "experienced contributor", minMergedPRs: 10 },
|
||||||
{ label: "trusted contributor", minMergedPRs: 5 },
|
{ 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 contributorTierLabels = contributorTierRules.map((rule) => rule.label);
|
||||||
const contributorTierColor = "2ED9FF"; // Keep in sync with .github/workflows/auto-response.yml
|
|
||||||
|
|
||||||
const managedPathLabels = [
|
const managedPathLabels = [
|
||||||
"docs",
|
"docs",
|
||||||
|
|
|
||||||
2
.github/workflows/pr-hygiene.yml
vendored
2
.github/workflows/pr-hygiene.yml
vendored
|
|
@ -26,7 +26,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const staleHours = Number(process.env.STALE_HOURS || "48");
|
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 marker = "<!-- pr-hygiene-nudge -->";
|
||||||
const owner = context.repo.owner;
|
const owner = context.repo.owner;
|
||||||
const repo = context.repo.repo;
|
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:
|
jobs:
|
||||||
audit:
|
audit:
|
||||||
name: Security Audit
|
name: Security Audit
|
||||||
runs-on: blacksmith-2vcpu-ubuntu-2404
|
uses: ./.github/workflows/rust-reusable.yml
|
||||||
timeout-minutes: 20
|
with:
|
||||||
steps:
|
timeout_minutes: 20
|
||||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
toolchain: stable
|
||||||
|
run_command: |
|
||||||
- uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
|
cargo install --locked cargo-audit --version 0.22.1
|
||||||
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
|
cargo audit
|
||||||
|
|
||||||
- name: Install cargo-audit
|
|
||||||
run: cargo install --locked cargo-audit --version 0.22.1
|
|
||||||
|
|
||||||
- name: Run cargo-audit
|
|
||||||
run: cargo audit
|
|
||||||
|
|
||||||
deny:
|
deny:
|
||||||
name: License & Supply Chain
|
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
|
days-before-pr-close: 7
|
||||||
stale-issue-label: stale
|
stale-issue-label: stale
|
||||||
stale-pr-label: stale
|
stale-pr-label: stale
|
||||||
exempt-issue-labels: security,pinned,no-stale,maintainer
|
exempt-issue-labels: security,pinned,no-stale,no-pr-hygiene,maintainer
|
||||||
exempt-pr-labels: no-stale,maintainer
|
exempt-pr-labels: no-stale,no-pr-hygiene,maintainer
|
||||||
remove-stale-when-updated: true
|
remove-stale-when-updated: true
|
||||||
exempt-all-assignees: true
|
exempt-all-assignees: true
|
||||||
operations-per-run: 300
|
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
|
# Run every Sunday at 00:00 UTC
|
||||||
- cron: '0 0 * * 0'
|
- cron: '0 0 * * 0'
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: update-notice-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
|
|
@ -13,10 +17,10 @@ permissions:
|
||||||
jobs:
|
jobs:
|
||||||
update-notice:
|
update-notice:
|
||||||
name: Update NOTICE with new contributors
|
name: Update NOTICE with new contributors
|
||||||
runs-on: ubuntu-latest
|
runs-on: blacksmith-2vcpu-ubuntu-2404
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
- name: Fetch contributors
|
- name: Fetch contributors
|
||||||
id: contributors
|
id: contributors
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,9 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
|
||||||
- `.github/workflows/release.yml` (`Release`)
|
- `.github/workflows/release.yml` (`Release`)
|
||||||
- Purpose: build tagged release artifacts and publish GitHub releases
|
- Purpose: build tagged release artifacts and publish GitHub releases
|
||||||
- `.github/workflows/label-policy-sanity.yml` (`Label Policy Sanity`)
|
- `.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
|
### Optional Repository Automation
|
||||||
|
|
||||||
|
|
@ -62,7 +64,7 @@ Merge-blocking checks should stay small and deterministic. Optional checks are u
|
||||||
- `Release`: tag push (`v*`)
|
- `Release`: tag push (`v*`)
|
||||||
- `Security Audit`: push to `main`, PRs to `main`, weekly schedule
|
- `Security Audit`: push to `main`, PRs to `main`, weekly schedule
|
||||||
- `Workflow Sanity`: PR/push when `.github/workflows/**`, `.github/*.yml`, or `.github/*.yaml` change
|
- `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 Labeler`: `pull_request_target` lifecycle events
|
||||||
- `PR Auto Responder`: issue opened/labeled, `pull_request_target` opened/labeled
|
- `PR Auto Responder`: issue opened/labeled, `pull_request_target` opened/labeled
|
||||||
- `Stale`: daily schedule, manual dispatch
|
- `Stale`: daily schedule, manual dispatch
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue