Skip to content

Commit a9dce82

Browse files
committed
Updated github action to be a two step workflow
1 parent 112efc4 commit a9dce82

2 files changed

Lines changed: 157 additions & 95 deletions

File tree

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
name: Template Compatibility Comment
2+
3+
# Triggered when the unprivileged "Template Compatibility Check" workflow
4+
# completes. This workflow has pull-requests: write so it can post PR comments,
5+
# but it never checks out or executes any external code — it only reads the
6+
# artifact produced by the check workflow.
7+
#
8+
# SECURITY: workflow_run always runs on the default branch, so this workflow
9+
# definition itself cannot be tampered with by a PR contributor. Artifact
10+
# contents are treated as untrusted strings and sanitized before use.
11+
12+
on:
13+
workflow_run:
14+
workflows: ["Template Compatibility Check"]
15+
types: [completed]
16+
17+
permissions:
18+
pull-requests: write
19+
actions: read # required to download artifacts from another workflow run
20+
21+
jobs:
22+
post-comment:
23+
runs-on: ubuntu-latest
24+
25+
steps:
26+
- name: Download results artifact
27+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
28+
with:
29+
name: template-compat-results
30+
path: /tmp/compat-results
31+
run-id: ${{ github.event.workflow_run.id }}
32+
github-token: ${{ secrets.GITHUB_TOKEN }}
33+
34+
- name: Post or remove PR comment
35+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
36+
with:
37+
script: |
38+
const fs = require('fs');
39+
40+
const read = (filename) => {
41+
try { return fs.readFileSync(`/tmp/compat-results/${filename}`, 'utf8').trim(); }
42+
catch { return ''; }
43+
};
44+
45+
// Validate PR number — must be a positive integer.
46+
const prNumber = parseInt(read('pr-number.txt'), 10);
47+
if (!Number.isInteger(prNumber) || prNumber <= 0) {
48+
console.log('Invalid or missing PR number in artifact; skipping comment.');
49+
return;
50+
}
51+
52+
const exitCode = read('exit-code.txt');
53+
const fullOutput = read('output.txt');
54+
// Sanitize values read from the artifact before embedding in markdown
55+
// to prevent injection (e.g. a malicious branch name or script output
56+
// containing markdown syntax that escapes a code fence).
57+
const templatesRef = read('templates-ref.txt').replace(/[^a-zA-Z0-9._\/-]/g, '');
58+
const headRef = read('head-ref.txt').replace(/[^a-zA-Z0-9._\/-]/g, '');
59+
60+
const marker = '<!-- template-compat-comment -->';
61+
62+
const { data: comments } = await github.rest.issues.listComments({
63+
owner: context.repo.owner,
64+
repo: context.repo.repo,
65+
issue_number: prNumber,
66+
});
67+
const existing = comments.find(c => c.body.includes(marker));
68+
69+
if (exitCode === '0') {
70+
// Templates pass — remove any stale failure comment.
71+
if (existing) {
72+
await github.rest.issues.deleteComment({
73+
owner: context.repo.owner,
74+
repo: context.repo.repo,
75+
comment_id: existing.id,
76+
});
77+
}
78+
return;
79+
}
80+
81+
// Extract just the "Results" and "Failure Details" sections from output.
82+
const resultsMatch = fullOutput.match(/={8,}\nResults:.*\n={8,}[\s\S]*/);
83+
const failureSummary = resultsMatch ? resultsMatch[0].trim() : fullOutput.trim();
84+
85+
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.payload.workflow_run.id}`;
86+
const refNote = templatesRef === 'main'
87+
? 'tested against `cre-templates:main`'
88+
: `tested against \`cre-templates:${templatesRef}\` (compat branch)`;
89+
90+
const body = [
91+
'## ⚠️ Template Compatibility Failures',
92+
'',
93+
`This PR breaks one or more templates in [cre-templates](https://github.com/smartcontractkit/cre-templates) (${refNote}).`,
94+
'',
95+
'```',
96+
failureSummary,
97+
'```',
98+
'',
99+
`[View full output →](${runUrl})`,
100+
'',
101+
'<details>',
102+
'<summary>What should I do?</summary>',
103+
'',
104+
'- **Accidental break:** Fix the SDK change so existing templates continue to compile.',
105+
`- **Intentional breaking change:** Create a branch in \`cre-templates\` named \`compat/${headRef}\` with the template fixes applied. This job will automatically retest against that branch.`,
106+
'',
107+
'</details>',
108+
].join('\n');
109+
110+
const commentBody = `${marker}\n${body}`;
111+
112+
if (existing) {
113+
await github.rest.issues.updateComment({
114+
owner: context.repo.owner,
115+
repo: context.repo.repo,
116+
comment_id: existing.id,
117+
body: commentBody,
118+
});
119+
} else {
120+
await github.rest.issues.createComment({
121+
owner: context.repo.owner,
122+
repo: context.repo.repo,
123+
issue_number: prNumber,
124+
body: commentBody,
125+
});
126+
}

.github/workflows/template-compatibility.yml

Lines changed: 31 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Template Compatibility
1+
name: Template Compatibility Check
22

33
on:
44
pull_request:
@@ -13,23 +13,17 @@ on:
1313

1414
permissions:
1515
contents: read
16-
pull-requests: write
1716

1817
jobs:
1918
template-compatibility:
2019
runs-on: ubuntu-latest
21-
# Only run on PRs (not push to main)
22-
if: github.event_name == 'pull_request'
2320

2421
defaults:
2522
run:
2623
shell: bash {0}
2724

2825
env:
2926
TEMPLATES_REPO: smartcontractkit/cre-templates
30-
# Convention: a matching branch in cre-templates named compat/<sdk-branch>
31-
# allows coordinated breaking changes without blocking the SDK PR.
32-
COMPAT_BRANCH: compat/${{ github.head_ref }}
3327

3428
steps:
3529
- name: Checkout SDK
@@ -74,7 +68,11 @@ jobs:
7468
id: detect-ref
7569
env:
7670
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
71+
HEAD_REF: ${{ github.head_ref }}
7772
run: |
73+
SAFE_HEAD_REF="${HEAD_REF//[^a-zA-Z0-9._\/-]/}"
74+
COMPAT_BRANCH="compat/$SAFE_HEAD_REF"
75+
7876
if gh api "repos/$TEMPLATES_REPO/branches/$COMPAT_BRANCH" &>/dev/null; then
7977
echo "ref=$COMPAT_BRANCH" >> "$GITHUB_OUTPUT"
8078
echo "Using compat branch: $COMPAT_BRANCH"
@@ -106,102 +104,40 @@ jobs:
106104
EXIT_CODE=$?
107105
set -e
108106
109-
# Save output to a file for the comment step
110107
echo "$OUTPUT" > /tmp/template-check-output.txt
111108
112109
# Surface it in the action log regardless
113110
echo "$OUTPUT"
114111
115112
echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT"
116113
117-
# Only post a comment when templates are broken.
118-
# The comment is updated (not duplicated) on subsequent pushes.
119-
- name: Post failure comment on PR
120-
if: steps.template-check.outputs.exit_code != '0'
121-
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
122-
with:
123-
script: |
124-
const fs = require('fs');
125-
const fullOutput = fs.readFileSync('/tmp/template-check-output.txt', 'utf8');
126-
127-
// Extract just the "Results" and "Failure Details" sections
128-
const resultsMatch = fullOutput.match(/={8,}\nResults:.*\n={8,}[\s\S]*/);
129-
const failureSummary = resultsMatch ? resultsMatch[0].trim() : fullOutput.trim();
130-
131-
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
132-
const templatesRef = '${{ steps.detect-ref.outputs.ref }}';
133-
const refNote = templatesRef === 'main'
134-
? 'tested against `cre-templates:main`'
135-
: `tested against \`cre-templates:${templatesRef}\` (compat branch)`;
136-
137-
const body = [
138-
'## ⚠️ Template Compatibility Failures',
139-
'',
140-
`This PR breaks one or more templates in [cre-templates](https://github.com/${{ env.TEMPLATES_REPO }}) (${refNote}).`,
141-
'',
142-
'```',
143-
failureSummary,
144-
'```',
145-
'',
146-
`[View full output →](${runUrl})`,
147-
'',
148-
'<details>',
149-
'<summary>What should I do?</summary>',
150-
'',
151-
'- **Accidental break:** Fix the SDK change so existing templates continue to compile.',
152-
'- **Intentional breaking change:** Create a branch in `cre-templates` named `compat/${{ github.head_ref }}` with the template fixes applied. This job will automatically retest against that branch.',
153-
'',
154-
'</details>',
155-
].join('\n');
156-
157-
const marker = '<!-- template-compat-comment -->';
158-
const commentBody = `${marker}\n${body}`;
159-
160-
// Update existing comment if present, otherwise create a new one
161-
const { data: comments } = await github.rest.issues.listComments({
162-
owner: context.repo.owner,
163-
repo: context.repo.repo,
164-
issue_number: context.issue.number,
165-
});
166-
167-
const existing = comments.find(c => c.body.includes(marker));
168-
if (existing) {
169-
await github.rest.issues.updateComment({
170-
owner: context.repo.owner,
171-
repo: context.repo.repo,
172-
comment_id: existing.id,
173-
body: commentBody,
174-
});
175-
} else {
176-
await github.rest.issues.createComment({
177-
owner: context.repo.owner,
178-
repo: context.repo.repo,
179-
issue_number: context.issue.number,
180-
body: commentBody,
181-
});
182-
}
183-
184-
# If a prior push had failures but this push fixes them, remove the stale comment
185-
- name: Remove stale failure comment (if now passing)
186-
if: steps.template-check.outputs.exit_code == '0'
187-
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
114+
# Bundle everything the comment workflow needs into an artifact.
115+
# The comment workflow runs with pull-requests: write but must never
116+
# execute external code — it only reads these files.
117+
- name: Save results for comment workflow
118+
if: always()
119+
env:
120+
PR_NUMBER: ${{ github.event.pull_request.number }}
121+
TEMPLATES_REF: ${{ steps.detect-ref.outputs.ref }}
122+
HEAD_REF: ${{ github.head_ref }}
123+
EXIT_CODE: ${{ steps.template-check.outputs.exit_code }}
124+
run: |
125+
mkdir -p /tmp/compat-results
126+
printf '%s' "$PR_NUMBER" > /tmp/compat-results/pr-number.txt
127+
printf '%s' "$TEMPLATES_REF" > /tmp/compat-results/templates-ref.txt
128+
printf '%s' "$HEAD_REF" > /tmp/compat-results/head-ref.txt
129+
printf '%s' "$EXIT_CODE" > /tmp/compat-results/exit-code.txt
130+
if [ -f /tmp/template-check-output.txt ]; then
131+
cp /tmp/template-check-output.txt /tmp/compat-results/output.txt
132+
fi
133+
134+
- name: Upload results artifact
135+
if: always()
136+
uses: actions/upload-artifact@ea165f8d68b2b1dc5da8e1af90a2a8f427aa4185 # v4.6.2
188137
with:
189-
script: |
190-
const marker = '<!-- template-compat-comment -->';
191-
const { data: comments } = await github.rest.issues.listComments({
192-
owner: context.repo.owner,
193-
repo: context.repo.repo,
194-
issue_number: context.issue.number,
195-
});
196-
197-
const existing = comments.find(c => c.body.includes(marker));
198-
if (existing) {
199-
await github.rest.issues.deleteComment({
200-
owner: context.repo.owner,
201-
repo: context.repo.repo,
202-
comment_id: existing.id,
203-
});
204-
}
138+
name: template-compat-results
139+
path: /tmp/compat-results/
140+
retention-days: 7
205141

206142
# Always exit 0 — this job is informational, not a merge gate
207143
- name: Report result (non-blocking)

0 commit comments

Comments
 (0)