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
62 changes: 57 additions & 5 deletions .github/workflows/required-quality-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,63 @@ concurrency:

jobs:
quality-gate:
name: quality-gate
uses: HMG-AI/.github/.github/workflows/reusable-commitlint.yml@978d6d08d6bfc1e8a3662c1f9b0daab8bfa0e557
with:
base_sha: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
head_sha: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
name: quality-gate / commitlint
runs-on: ubuntu-latest
timeout-minutes: 10
container:
image: node:24.11.1-bookworm@sha256:9a2ed90cd91b1f3412affe080b62e69b057ba8661d9844e143a6bbd76a23260f
options: --security-opt=no-new-privileges
steps:
- name: Validate commit SHA inputs
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
run: |
set -Eeuo pipefail
readonly sha_pattern='^[0-9a-f]{40}$'

for sha_name in BASE_SHA HEAD_SHA; do
sha_value="${!sha_name}"
if [[ ! "${sha_value}" =~ ${sha_pattern} ]]; then
echo "::error::${sha_name} must be a full 40-character lowercase hexadecimal commit SHA"
exit 2
fi
done

- name: Check out caller repository history
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
path: caller
persist-credentials: false

- name: Check out immutable organization commit policy
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
repository: HMG-AI/.github
ref: ${{ github.workflow_sha }}
path: governance-source
sparse-checkout: governance/commitlint
persist-credentials: false

- name: Install locked commitlint dependencies
shell: bash
working-directory: governance-source/governance/commitlint
run: npm ci --ignore-scripts --no-audit --no-fund

- name: Lint every commit in base..head
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.merge_group.head_sha }}
run: |
set -Eeuo pipefail
bash "${GITHUB_WORKSPACE}/governance-source/governance/commitlint/lint-range.sh" \
"${GITHUB_WORKSPACE}/caller" \
"${BASE_SHA}" \
"${HEAD_SHA}"

workflow-security-policy:
name: workflow-security-policy
Expand Down
26 changes: 26 additions & 0 deletions governance/commitlint/commitlint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,26 @@ const noHan = (field) => (parsed) => [
`${field} must not contain Unicode Han script characters`,
];

const canonicalLongProvenanceTrailers = [
/^HMG-Provenance-Key-ID: ed25519-spki-sha256-[0-9a-f]{64}$/,
/^HMG-Provenance-Signature-Ed25519: [A-Za-z0-9+/]{86}==$/,
];

const boundedFooterLines = (parsed, _when, limit = 100) => {
const invalidLines = (parsed.footer ?? '')
.split('\n')
.filter(
(line) =>
line.length > limit &&
!canonicalLongProvenanceTrailers.some((pattern) => pattern.test(line)),
);

return [
invalidLines.length === 0,
`footer lines must not exceed ${limit} characters unless they are canonical HMG provenance trailers`,
];
};

export default {
...conventional,
plugins: [
Expand All @@ -15,11 +35,17 @@ export default {
'subject-no-han': noHan('subject'),
'body-no-han': noHan('body'),
'footer-no-han': noHan('footer'),
'footer-bounded-lines': boundedFooterLines,
},
},
],
rules: {
...conventional.rules,
// The two canonical cryptographic trailers exceed the conventional
// 100-character limit. Disable the broad built-in rule and replace it
// with a strict shape-aware exception.
'footer-max-line-length': [0],
'footer-bounded-lines': [2, 'always', 100],
'subject-no-han': [2, 'always'],
'body-no-han': [2, 'always'],
'footer-no-han': [2, 'always'],
Expand Down
26 changes: 26 additions & 0 deletions governance/commitlint/test/commitlint.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,32 @@ test('accepts a conventional English commit message', () => {
assert.equal(result.status, 0, result.stdout || result.stderr);
});

test('accepts canonical cryptographic provenance trailers', () => {
const message = [
'chore(public): promote HMG v1.7.7',
'',
`HMG-Provenance-Key-ID: ed25519-spki-sha256-${'a'.repeat(64)}`,
`HMG-Provenance-Signature-Ed25519: ${'A'.repeat(86)}==`,
'',
].join('\n');
const result = lint(message);
assert.equal(result.status, 0, result.stdout || result.stderr);
});

test('rejects an ordinary footer line longer than 100 characters', () => {
assertLintFailure(
`chore: reject an oversized footer\n\nAudit-Value: ${'a'.repeat(130)}\n`,
'footer-bounded-lines',
);
});

test('rejects a long provenance-shaped trailer with a malformed value', () => {
assertLintFailure(
`chore(public): reject malformed provenance\n\nHMG-Provenance-Signature-Ed25519: ${'A'.repeat(85)}===\n`,
'footer-bounded-lines',
);
});

test('rejects Unicode Han script in subject, body, and footer', () => {
assertLintFailure('fix: 修复 tenant lookup\n', 'subject-no-han');
assertLintFailure(
Expand Down