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
44 changes: 44 additions & 0 deletions .github/workflows/template-compatibility-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Template Compatibility Comment

# Triggered when the unprivileged "Template Compatibility Check" workflow
# completes. This workflow has pull-requests: write so it can post PR comments.
# It checks out the default branch (only) to load scripts/template-compatibility-comment.js,
# then reads the artifact produced by the check workflow.
#
# SECURITY: workflow_run always runs on the default branch, so this workflow
# definition and checked-out script cannot be tampered with by a PR contributor.
# Artifact contents are treated as untrusted strings and sanitized before use.

on:
workflow_run:
workflows: ["Template Compatibility Check"]
types: [completed]

permissions:
contents: read
pull-requests: write
actions: read # required to download artifacts from another workflow run

jobs:
post-comment:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Download results artifact
uses: actions/download-artifact@v8
with:
name: template-compat-results
path: /tmp/compat-results
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Post or remove PR comment
uses: actions/github-script@v8
with:
script: |
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel it would be cleaner and easier to maintain if we put script in file together with other scripts we had and just call it here in 1-2 lines.

const path = require('path');
const run = require(path.join(process.env.GITHUB_WORKSPACE, 'scripts', 'template-compatibility-comment.js'));
await run({ github, context });
158 changes: 158 additions & 0 deletions .github/workflows/template-compatibility.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
name: Template Compatibility Check

on:
pull_request:
types: [opened, synchronize, reopened]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also add

  concurrency:
    group: template-compat-${{ github.head_ref || github.ref }}
    cancel-in-progress: true

As template testing could take some time and upon multiple commits we would have improved efficiency.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call

concurrency:
group: template-compat-${{ github.head_ref || github.ref }}
cancel-in-progress: true

# This job is informational — it is intentionally NOT a required status check
# so that breaking SDK changes (major version bumps) can still be merged.
# When templates break, a comment is posted on the PR with details.
# For intentional breaking changes, create a matching branch in cre-templates
# named compat/<your-sdk-branch-name> with the template fixes applied.
# The job will automatically detect and test against that branch.

permissions:
contents: read

jobs:
template-compatibility:
runs-on: ubuntu-latest
timeout-minutes: 20

defaults:
run:
shell: bash {0}

env:
TEMPLATES_REPO: smartcontractkit/cre-templates

steps:
- name: Checkout SDK
uses: actions/checkout@v6
with:
submodules: recursive

- name: Setup Rust (1.85.0) with wasm target
uses: actions-rust-lang/setup-rust-toolchain@150fca883cd4034361b621bd4e6a9d34e5143606 # v1.15.4
with:
toolchain: 1.85.0
target: wasm32-wasip1
override: true

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.12

- name: Cache Bun dependencies
uses: actions/cache@v5
with:
path: ~/.bun/install/cache
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}

- name: Cache cargo + Javy
uses: actions/cache@v5
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cache/javy
key: ${{ runner.os }}-cre-plugin-v8.1.0-${{ hashFiles('packages/cre-sdk-javy-plugin/src/javy_chainlink_sdk/Cargo.lock', 'packages/cre-sdk-javy-plugin/src/cre_generated_host.Cargo.lock') }}

- name: Install SDK dependencies
run: bun install --frozen-lockfile

# Detect whether a matching compat branch exists in cre-templates.
# If it does, we test against it (coordinated breaking change).
# If not, we fall back to main.
- name: Detect cre-templates ref to test against
id: detect-ref
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HEAD_REF: ${{ github.head_ref }}
run: |
SAFE_HEAD_REF="${HEAD_REF//[^a-zA-Z0-9._\/-]/}"
COMPAT_BRANCH="compat/$SAFE_HEAD_REF"

if gh api "repos/$TEMPLATES_REPO/branches/$COMPAT_BRANCH" &>/dev/null; then
echo "ref=$COMPAT_BRANCH" >> "$GITHUB_OUTPUT"
echo "Using compat branch: $COMPAT_BRANCH"
else
echo "ref=main" >> "$GITHUB_OUTPUT"
echo "No compat branch found, using: main"
fi

- name: Checkout cre-templates (${{ steps.detect-ref.outputs.ref }})
uses: actions/checkout@v6
with:
repository: ${{ env.TEMPLATES_REPO }}
ref: ${{ steps.detect-ref.outputs.ref }}
path: cre-templates
token: ${{ secrets.GITHUB_TOKEN }}

- name: Setup Node (for npm)

Check warning

Code scanning / CodeQL

Checkout of untrusted code in trusted context Medium

Potential unsafe checkout of untrusted pull request on privileged workflow.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment on lines +89 to +97
uses: actions/setup-node@v6
with:
node-version: '24'

- name: Run template compatibility check
id: template-check
env:
TEMPLATES_DIR: cre-templates
run: |
set +e
OUTPUT=$(./scripts/test-templates.sh 2>&1)
EXIT_CODE=$?
set -e

echo "$OUTPUT" > /tmp/template-check-output.txt

# Surface it in the action log regardless
echo "$OUTPUT"

echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT"

# Bundle everything the comment workflow needs into an artifact.
# The comment workflow runs with pull-requests: write but must never
# execute external code — it only reads these files.
- name: Save results for comment workflow
if: always()
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
TEMPLATES_REF: ${{ steps.detect-ref.outputs.ref }}
HEAD_REF: ${{ github.head_ref }}
EXIT_CODE: ${{ steps.template-check.outputs.exit_code }}
run: |
mkdir -p /tmp/compat-results
printf '%s' "$PR_NUMBER" > /tmp/compat-results/pr-number.txt
printf '%s' "$TEMPLATES_REF" > /tmp/compat-results/templates-ref.txt
printf '%s' "$HEAD_REF" > /tmp/compat-results/head-ref.txt
printf '%s' "$EXIT_CODE" > /tmp/compat-results/exit-code.txt
if [ -f /tmp/template-check-output.txt ]; then
cp /tmp/template-check-output.txt /tmp/compat-results/output.txt
fi

- name: Upload results artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: template-compat-results
path: /tmp/compat-results/
retention-days: 7

# Always exit 0 — this job is informational, not a merge gate
- name: Report result (non-blocking)
if: always()
run: |
EXIT_CODE="${{ steps.template-check.outputs.exit_code }}"
if [ "$EXIT_CODE" = "0" ]; then
echo "✅ All templates are compatible with this SDK change."
else
echo "⚠️ Some templates failed — see PR comment for details."
echo "This check is informational and does not block merging."
fi
exit 0
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"check:ci": "turbo run check:ci",
"format": "turbo run format",
"full-checks": "./scripts/full-checks.sh",
"test:templates": "./scripts/test-templates.sh",
"lint": "turbo run lint",
"typecheck": "turbo run typecheck"
},
Expand Down
100 changes: 100 additions & 0 deletions scripts/template-compatibility-comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Used by .github/workflows/template-compatibility-comment.yml.
* Reads /tmp/compat-results from the prior workflow's artifact and posts or
* removes a PR comment via the GitHub API.
*/
module.exports = async ({ github, context }) => {
const fs = require('fs');

const read = (filename) => {
try {
return fs.readFileSync(`/tmp/compat-results/${filename}`, 'utf8').trim();
} catch {
return '';
}
};

// Validate PR number — must be a positive integer.
const prNumber = parseInt(read('pr-number.txt'), 10);
if (!Number.isInteger(prNumber) || prNumber <= 0) {
console.log('Invalid or missing PR number in artifact; skipping comment.');
return;
}

const exitCode = read('exit-code.txt');
const fullOutput = read('output.txt');
// Sanitize values read from the artifact before embedding in markdown
// to prevent injection (e.g. a malicious branch name or script output
// containing markdown syntax that escapes a code fence).
const templatesRef = read('templates-ref.txt').replace(/[^a-zA-Z0-9._\/-]/g, '');
const headRef = read('head-ref.txt').replace(/[^a-zA-Z0-9._\/-]/g, '');

const marker = '<!-- template-compat-comment -->';

const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
});
const existing = comments.find((c) => c.body.includes(marker));

if (exitCode === '0') {
// Templates pass — remove any stale failure comment.
if (existing) {
await github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
});
}
return;
}

// Extract just the "Results" and "Failure Details" sections from output.
const resultsMatch = fullOutput.match(/={8,}\nResults:.*\n={8,}[\s\S]*/);
const failureSummary = resultsMatch ? resultsMatch[0].trim() : fullOutput.trim();

const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.payload.workflow_run.id}`;
const refNote =
templatesRef === 'main'
? 'tested against `cre-templates:main`'
: `tested against \`cre-templates:${templatesRef}\` (compat branch)`;

const body = [
'## ⚠️ Template Compatibility Failures',
'',
`This PR breaks one or more templates in [cre-templates](https://github.com/smartcontractkit/cre-templates) (${refNote}).`,
'',
'```',
failureSummary,
'```',
'',
`[View full output →](${runUrl})`,
'',
'<details>',
'<summary>What should I do?</summary>',
'',
'- **Accidental break:** Fix the SDK change so existing templates continue to compile.',
`- **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.`,
'',
'</details>',
].join('\n');

const commentBody = `${marker}\n${body}`;

if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: commentBody,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body: commentBody,
});
}
};
Loading
Loading