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
9 changes: 8 additions & 1 deletion .github/workflows/release-manifest-contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:
paths:
- ".github/actionlint.yaml"
- ".github/workflows/release-manifest-contract.yml"
- ".github/workflows/sentrux-quality-gate.yml"
- "tests/workflow_sentrux_shell_test.py"
- ".github/workflows/release-coordinator.yml"
- ".github/workflows/release-comprehensive.yml"
- ".github/workflows/publish-npm.yml"
Expand Down Expand Up @@ -60,6 +62,8 @@ on:
paths:
- ".github/actionlint.yaml"
- ".github/workflows/release-manifest-contract.yml"
- ".github/workflows/sentrux-quality-gate.yml"
- "tests/workflow_sentrux_shell_test.py"
- ".github/workflows/release-coordinator.yml"
- ".github/workflows/release-comprehensive.yml"
- ".github/workflows/publish-npm.yml"
Expand Down Expand Up @@ -216,7 +220,7 @@ jobs:
- name: actionlint (static workflow analysis)
run: |
set -euo pipefail
actionlint .github/workflows/publish-crates.yml .github/workflows/lsp-public-ci.yml .github/workflows/publish-lsp-crates.yml .github/workflows/lsp-producer.yml .github/workflows/lsp-publish.yml .github/workflows/release-coordinator.yml .github/workflows/release-sign.yml .github/workflows/release-comprehensive.yml .github/workflows/publish-npm.yml .github/workflows/publish-pypi.yml .github/workflows/publish-bun.yml .github/workflows/publish-tauri.yml .github/workflows/release-manifest-contract.yml
actionlint .github/workflows/sentrux-quality-gate.yml .github/workflows/publish-crates.yml .github/workflows/lsp-public-ci.yml .github/workflows/publish-lsp-crates.yml .github/workflows/lsp-producer.yml .github/workflows/lsp-publish.yml .github/workflows/release-coordinator.yml .github/workflows/release-sign.yml .github/workflows/release-comprehensive.yml .github/workflows/publish-npm.yml .github/workflows/publish-pypi.yml .github/workflows/publish-bun.yml .github/workflows/publish-tauri.yml .github/workflows/release-manifest-contract.yml

- name: Parse workflow YAML
run: |
Expand All @@ -231,6 +235,9 @@ jobs:
- name: Run coordinator state-machine contract tests
run: python -m unittest -v tests.release_coordinator_test

- name: Exercise Sentrux shell output handling
run: python -m unittest -v tests.workflow_sentrux_shell_test

- name: Run workflow static contract tests
run: python -m unittest -v tests.workflow_release_coordinator_contract_test

Expand Down
62 changes: 32 additions & 30 deletions .github/workflows/sentrux-quality-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,25 @@ jobs:

# Extract quality signal
QUALITY=$(echo "$OUTPUT" | grep "Quality:" | awk '{print $2}')
echo "quality=$QUALITY" >> $GITHUB_OUTPUT
echo "quality=$QUALITY" >> "$GITHUB_OUTPUT"

# Count violations
VIOLATIONS=$(echo "$OUTPUT" | grep "violation(s) found" | awk '{print $2}')
echo "violations=$VIOLATIONS" >> $GITHUB_OUTPUT
echo "violations=$VIOLATIONS" >> "$GITHUB_OUTPUT"

# Determine status
if echo "$OUTPUT" | grep -q "✗.*violation"; then
echo "status=fail" >> $GITHUB_OUTPUT
echo "status=fail" >> "$GITHUB_OUTPUT"
else
echo "status=pass" >> $GITHUB_OUTPUT
echo "status=pass" >> "$GITHUB_OUTPUT"
fi

# Save full output for comment
echo "output<<EOF" >> $GITHUB_OUTPUT
echo "$OUTPUT" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
{
echo "output<<EOF"
echo "$OUTPUT"
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Fetch Baseline (PR only)
if: github.event_name == 'pull_request'
Expand All @@ -59,10 +61,10 @@ jobs:
python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('content','').strip())" 2>/dev/null || echo "0")

if [ "$BASELINE" = "0" ] || [ -z "$BASELINE" ]; then
echo "baseline=0" >> $GITHUB_OUTPUT
echo "baseline=0" >> "$GITHUB_OUTPUT"
echo "No baseline found"
else
echo "baseline=$BASELINE" >> $GITHUB_OUTPUT
echo "baseline=$BASELINE" >> "$GITHUB_OUTPUT"
echo "Baseline: $BASELINE"
fi

Expand All @@ -74,16 +76,16 @@ jobs:
BASELINE=${{ steps.baseline.outputs.baseline }}

if [ "$BASELINE" = "0" ] || [ -z "$BASELINE" ]; then
echo "delta=unknown" >> $GITHUB_OUTPUT
echo "degraded=false" >> $GITHUB_OUTPUT
echo "delta=unknown" >> "$GITHUB_OUTPUT"
echo "degraded=false" >> "$GITHUB_OUTPUT"
else
DELTA=$((CURRENT - BASELINE))
echo "delta=$DELTA" >> $GITHUB_OUTPUT
echo "delta=$DELTA" >> "$GITHUB_OUTPUT"

if [ $DELTA -lt -100 ]; then
echo "degraded=true" >> $GITHUB_OUTPUT
if [ "$DELTA" -lt -100 ]; then
echo "degraded=true" >> "$GITHUB_OUTPUT"
else
echo "degraded=false" >> $GITHUB_OUTPUT
echo "degraded=false" >> "$GITHUB_OUTPUT"
fi
fi

Expand Down Expand Up @@ -133,33 +135,33 @@ jobs:

COMMENT="## Sentrux Quality Gate $STATUS_ICON

**Quality Signal:** $QUALITY / 10000
**Violations:** $VIOLATIONS
**Status:** $STATUS
"
**Quality Signal:** $QUALITY / 10000
**Violations:** $VIOLATIONS
**Status:** $STATUS
"

if [ "$DELTA" != "unknown" ]; then
COMMENT="${COMMENT}**Delta:** $DELTA_ICON $DELTA points (baseline: $BASELINE)
"
"
if [ "$DEGRADED" = "true" ]; then
COMMENT="${COMMENT}
🔴 **Quality degraded by more than 100 points!**
"
🔴 **Quality degraded by more than 100 points!**
"
fi
else
COMMENT="${COMMENT}**Delta:** No baseline available
"
"
fi

COMMENT="${COMMENT}
<details>
<summary>Full Report</summary>
<details>
<summary>Full Report</summary>

\`\`\`
${{ steps.sentrux.outputs.output }}
\`\`\`
</details>
"
\`\`\`
${{ steps.sentrux.outputs.output }}
\`\`\`
</details>
"

# Post comment
curl -s -X POST -H "Authorization: token $GITEA_TOKEN" \
Expand Down
78 changes: 78 additions & 0 deletions tests/workflow_sentrux_shell_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Run Sentrux workflow output handling in real Bash, without external services."""

from pathlib import Path
import os
import subprocess
import tempfile
import unittest


WORKFLOW = Path(__file__).resolve().parents[1] / ".github/workflows/sentrux-quality-gate.yml"


def run_block(step):
"""Extract one literal run block, rejecting content outside its indentation."""
body = WORKFLOW.read_text().split(f" - name: {step}\n", 1)[1]
body = body.split("\n - name:", 1)[0].split(" run: |\n", 1)[1]
lines = body.splitlines()
if any(line.strip() and not line.startswith(" ") for line in lines):
raise ValueError(f"Run block contains unindented content: {step}")
return "\n".join(line[10:] if line.startswith(" ") else line for line in lines) + "\n"


class SentruxShellTests(unittest.TestCase):
def output_file(self, script, **variables):
"""Execute a pure workflow fragment against a real file with spaces."""
with tempfile.TemporaryDirectory(prefix="sentrux workflow ") as folder:
output = Path(folder) / "step output.txt"
output.write_text("existing=value\n")
environment = dict(os.environ, GITHUB_OUTPUT=str(output), **variables)
subprocess.run(["bash", "-e", "-c", script], env=environment,
check=True, capture_output=True, text=True, timeout=10)
return output.read_text()

def test_report_extraction_appends_exact_multiline_output(self):
# Exercise the real parsing/redirect fragment; do not replace the Sentrux binary.
script = run_block("Run Sentrux Check").split("# Extract quality signal\n", 1)[1]
for report, quality, count, status in [
("Quality: 9600\n✓ 0 violation(s) found", "9600", "0", "pass"),
("Quality: 8000\n✗ 2 violation(s) found\nDetail: a * literal [path]", "8000", "2", "fail"),
]:
with self.subTest(status=status):
self.assertEqual(
self.output_file(script, OUTPUT=report),
f"existing=value\nquality={quality}\nviolations={count}\nstatus={status}\n"
f"output<<EOF\n{report}\nEOF\n",
)

def test_baseline_redirect_preserves_zero_empty_and_known_values(self):
script = run_block("Fetch Baseline (PR only)").split('if [ "$BASELINE"', 1)[1]
script = 'if [ "$BASELINE"' + script
for value, expected in [("", "0"), ("0", "0"), ("9600", "9600")]:
with self.subTest(value=value):
self.assertEqual(self.output_file(script, BASELINE=value),
f"existing=value\nbaseline={expected}\n")

def test_delta_boundary_and_output_path(self):
template = run_block("Calculate Delta (PR only)")
for current, baseline, delta, degraded in [
("9600", "0", "unknown", "false"),
("9600", "", "unknown", "false"),
("9600", "9600", "0", "false"),
("9500", "9600", "-100", "false"),
("9499", "9600", "-101", "true"),
]:
with self.subTest(current=current, baseline=baseline):
script = template.replace("${{ steps.sentrux.outputs.quality }}", current)
script = script.replace("${{ steps.baseline.outputs.baseline }}", baseline)
self.assertEqual(self.output_file(script),
f"existing=value\ndelta={delta}\ndegraded={degraded}\n")

def test_comment_body_stays_inside_literal_run_block(self):
script = run_block("Post PR Comment")
self.assertIn("\n**Quality Signal:** $QUALITY / 10000\n", script)
self.assertIn("\n<details>\n<summary>Full Report</summary>\n", script)


if __name__ == "__main__":
unittest.main()
Loading