Skip to content

Commit 2be80ed

Browse files
fix(ci): ruff format + idempotent GitLab release
GitHub Security pipeline: - Apply ruff format to mcp_server.py, test_gitlab.py, test_mcp.py (format --check was failing on all three files) GitLab pipeline: - Replace declarative 'release:' keyword with REST API call in create-release job — now checks if release already exists before creating. This prevents cascading failures when the GitHub→GitLab sync workflow force-pushes tags (every re-push triggered a new tag pipeline that tried to re-create the same release). - Switch from glab CLI image to python:3.11-slim (lighter, matches other jobs) — uses curl + CI_JOB_TOKEN for the API call. - Strip 'v' prefix for pip install version in release description.
1 parent a99d663 commit 2be80ed

File tree

4 files changed

+57
-34
lines changed

4 files changed

+57
-34
lines changed

.gitlab-ci.yml

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -56,34 +56,42 @@ test:
5656
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
5757

5858
# --- Publish release to CI/CD Catalog ---
59+
# Idempotent: if the release already exists (e.g. from a re-synced tag),
60+
# the job succeeds immediately. This prevents cascading failures when
61+
# the GitHub→GitLab sync workflow force-pushes tags.
5962
create-release:
6063
stage: release
61-
image: registry.gitlab.com/gitlab-org/cli:latest
64+
image: python:3.11-slim
6265
script:
63-
- echo "Publishing release $CI_COMMIT_TAG to CI/CD Catalog"
64-
rules:
65-
- if: $CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+$/
66-
allow_failure: false
67-
release:
68-
tag_name: $CI_COMMIT_TAG
69-
description: |
70-
## AIIR $CI_COMMIT_TAG
71-
72-
AI Integrity Receipts — tamper-evident cryptographic receipts for every AI-generated commit.
66+
- |
67+
echo "Checking release status for $CI_COMMIT_TAG"
7368
74-
### Install
69+
# Check if release already exists (REST API with CI_JOB_TOKEN)
70+
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
71+
--header "JOB-TOKEN: $CI_JOB_TOKEN" \
72+
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases/${CI_COMMIT_TAG}")
7573
76-
```
77-
pip install aiir==$CI_COMMIT_TAG
78-
```
74+
if [ "$STATUS" = "200" ]; then
75+
echo "✅ Release $CI_COMMIT_TAG already exists — skipping"
76+
exit 0
77+
fi
7978
80-
### Use as CI/CD Component
79+
# Strip 'v' prefix for pip install version
80+
VERSION="${CI_COMMIT_TAG#v}"
8181
82-
```yaml
83-
include:
84-
- component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/receipt@$CI_COMMIT_TAG
85-
```
82+
# Create release via REST API
83+
curl -s --fail \
84+
--header "JOB-TOKEN: $CI_JOB_TOKEN" \
85+
--header "Content-Type: application/json" \
86+
--data "{
87+
\"tag_name\": \"${CI_COMMIT_TAG}\",
88+
\"name\": \"AIIR ${CI_COMMIT_TAG}\",
89+
\"description\": \"## AIIR ${CI_COMMIT_TAG}\\n\\nAI Integrity Receipts — tamper-evident cryptographic receipts for every AI-generated commit.\\n\\n### Install\\n\\n\\\`\\\`\\\`\\npip install aiir==${VERSION}\\n\\\`\\\`\\\`\\n\\n### Use as CI/CD Component\\n\\n\\\`\\\`\\\`yaml\\ninclude:\\n - component: ${CI_SERVER_FQDN}/${CI_PROJECT_PATH}/receipt@${CI_COMMIT_TAG}\\n\\\`\\\`\\\`\\n\\n### Changelog\\n\\nSee [CHANGELOG.md](https://github.com/invariant-systems-ai/aiir/blob/main/CHANGELOG.md)\"
90+
}" \
91+
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases"
8692
87-
### Changelog
88-
89-
See [CHANGELOG.md](https://github.com/invariant-systems-ai/aiir/blob/main/CHANGELOG.md)
93+
echo ""
94+
echo "✅ Release $CI_COMMIT_TAG created"
95+
rules:
96+
- if: $CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+$/
97+
allow_failure: false

aiir/mcp_server.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -593,9 +593,7 @@ def _handle_aiir_gitlab_summary(args: Dict[str, Any]) -> Dict[str, Any]:
593593
sast = format_gl_sast_report(receipts)
594594
summary += (
595595
"\n\n<details>\n<summary>🛡️ SAST Report Data</summary>\n\n"
596-
"```json\n"
597-
+ json.dumps(sast, indent=2)
598-
+ "\n```\n\n</details>"
596+
"```json\n" + json.dumps(sast, indent=2) + "\n```\n\n</details>"
599597
)
600598

601599
# Optionally post to MR (best-effort, non-fatal)

tests/test_gitlab.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,9 @@ def test_successful_query(self):
759759
self.assertEqual(result, {"currentUser": {"name": "test-user"}})
760760
mock_url.assert_called_once()
761761
req = mock_url.call_args[0][0]
762-
self.assertEqual(req.get_header("Authorization"), "Bearer glpat-test123")
762+
self.assertEqual(
763+
req.get_header("Authorization"), "Bearer glpat-test123"
764+
)
763765
self.assertIn(b"currentUser", req.data)
764766

765767
def test_custom_api_url(self):
@@ -784,9 +786,9 @@ def test_custom_api_url(self):
784786

785787
def test_graphql_errors_raise(self):
786788
"""GraphQL errors in response raise RuntimeError."""
787-
response_data = json.dumps(
788-
{"errors": [{"message": "Syntax error"}]}
789-
).encode("utf-8")
789+
response_data = json.dumps({"errors": [{"message": "Syntax error"}]}).encode(
790+
"utf-8"
791+
)
790792

791793
mock_resp = unittest.mock.MagicMock()
792794
mock_resp.read.return_value = response_data

tests/test_mcp.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,8 +293,16 @@ def _make_test_receipt(
293293
"version": CLI_VERSION,
294294
"commit": {
295295
"sha": sha,
296-
"author": {"name": "Test", "email": "t@e.com", "date": "2026-01-01T00:00:00Z"},
297-
"committer": {"name": "Test", "email": "t@e.com", "date": "2026-01-01T00:00:00Z"},
296+
"author": {
297+
"name": "Test",
298+
"email": "t@e.com",
299+
"date": "2026-01-01T00:00:00Z",
300+
},
301+
"committer": {
302+
"name": "Test",
303+
"email": "t@e.com",
304+
"date": "2026-01-01T00:00:00Z",
305+
},
298306
"subject": subject,
299307
"message_hash": "sha256:aaa",
300308
"diff_hash": "sha256:bbb",
@@ -364,7 +372,12 @@ def test_summary_from_ledger(self):
364372

365373
receipts = [
366374
_make_test_receipt(sha="aaa111", subject="feat: AI feature", is_ai=True),
367-
_make_test_receipt(sha="bbb222", subject="fix: human fix", is_ai=False, authorship_class="human"),
375+
_make_test_receipt(
376+
sha="bbb222",
377+
subject="fix: human fix",
378+
is_ai=False,
379+
authorship_class="human",
380+
),
368381
]
369382

370383
with tempfile.TemporaryDirectory() as tmpdir:
@@ -457,7 +470,9 @@ def test_post_to_mr_in_ci_context(self):
457470
"GITLAB_TOKEN": "test-token",
458471
}
459472
with unittest.mock.patch.dict(os.environ, env, clear=True):
460-
with unittest.mock.patch("aiir.mcp_server.post_mr_comment") as mock_post:
473+
with unittest.mock.patch(
474+
"aiir.mcp_server.post_mr_comment"
475+
) as mock_post:
461476
mock_post.return_value = {"id": 1}
462477
result = _handle_aiir_gitlab_summary({"post_to_mr": True})
463478
mock_post.assert_called_once()

0 commit comments

Comments
 (0)