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
7 changes: 1 addition & 6 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
version: 2
updates:
- package-ecosystem: cargo
directory: "/data/render"
directory: "/ci"
schedule:
interval: daily
time: "11:00"
open-pull-requests-limit: 10
ignore:
- dependency-name: tokio
versions:
- 1.1.1
- 1.2.0
79 changes: 76 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,84 @@ name: CI
on:
pull_request:
branches: [master]
push:
branches: [master]
paths:
- "ci/**"
- ".github/workflows/ci.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
build:
rust:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt
- name: Check formatting
run: cargo fmt --manifest-path ci/Cargo.toml --all -- --check
- name: Check workspace
run: cargo check --locked --manifest-path ci/Cargo.toml --workspace --all-targets
- name: Test workspace
run: cargo test --locked --manifest-path ci/Cargo.toml --workspace

readme-check:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Check README.md was not edited directly
env:
AUTHOR: ${{ github.event.pull_request.user.login }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
case "$AUTHOR" in mre|jakubsacha) exit 0 ;; esac
if ! git diff --quiet "$BASE_SHA...$HEAD_SHA" -- README.md; then
mkdir -p ci-readme-output
printf '%s\n' 'README.md was edited directly. It is generated from the YAML files in data/tools/. Please edit those files instead and revert the README change.' > ci-readme-output/comment.md
echo 'README.md must not be edited directly.' >&2
exit 1
fi
- name: Upload README feedback
if: failure()
uses: actions/upload-artifact@v4
with:
name: ci-readme-output
path: ci-readme-output/comment.md
if-no-files-found: ignore
retention-days: 7

render:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Render list
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dtolnay/rust-toolchain@stable
- name: Render list without credentials
id: render
# Fork code must not receive a token. The renderer supports anonymous mode.
run: make render-skip-deprecated
- name: Prepare render feedback
if: failure() && steps.render.outcome == 'failure' && github.event_name == 'pull_request'
run: |
mkdir -p ci-render-output
printf '%s\n' 'The catalog renderer failed. Please check the workflow logs and compare your YAML with the other files in data/tools/.' > ci-render-output/comment.md
- name: Upload render feedback
if: failure() && github.event_name == 'pull_request'
uses: actions/upload-artifact@v4
with:
name: ci-render-output
path: ci-render-output/comment.md
if-no-files-found: ignore
retention-days: 7
122 changes: 122 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: PR Check

on:
pull_request:
branches: [master]
paths:
- "data/tools/**.yml"
- "data/tools/**.yaml"
- "ci/**"
- ".github/workflows/pr-check.yml"
- ".github/workflows/pr-comment.yml"
workflow_dispatch:
inputs:
pr_number:
description: "PR number to check (results are uploaded as an artifact)"
required: true
type: string
tool_files:
description: "Space-separated tool YAML paths at the PR head; quote paths containing spaces"
required: true
type: string

permissions:
contents: read

jobs:
pr-check:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false

- name: Select tool files
env:
TOOL_FILES: ${{ inputs.tool_files }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
shell: python
run: |
import json
import os
from pathlib import Path, PurePosixPath
import re
import shlex
import subprocess

if not re.fullmatch(r"[1-9][0-9]*", os.environ["PR_NUMBER"]):
raise SystemExit("PR number must be a positive integer")
manual = os.environ["GITHUB_EVENT_NAME"] == "workflow_dispatch"
if manual:
files = shlex.split(os.environ["TOOL_FILES"])
if not files:
raise SystemExit("Provide at least one tool file")
else:
diff = subprocess.check_output([
"git", "diff", "--name-only", "--diff-filter=A", "-z",
os.environ["BASE_SHA"] + "..." + os.environ["HEAD_SHA"],
"--", "data/tools/*.yml", "data/tools/*.yaml",
])
files = [os.fsdecode(path) for path in diff.split(b"\0") if path]
for name in files:
path = PurePosixPath(name)
if (path.parts[:2] != ("data", "tools") or ".." in path.parts
or path.suffix not in (".yml", ".yaml") or ":" in name):
raise SystemExit("Only paths under data/tools ending in .yml or .yaml are allowed")
if manual:
subprocess.run([
"git", "fetch", "origin",
"refs/pull/" + os.environ["PR_NUMBER"] + "/head",
], check=True)
# Do not execute the PR's Rust code or workflow on manual dispatch.
subprocess.run(["git", "--literal-pathspecs", "checkout", "FETCH_HEAD", "--", *files], check=True)
root = Path("data/tools").resolve()
for name in files:
path = Path(name)
if path.is_symlink() or not path.is_file() or not path.resolve().is_relative_to(root):
raise SystemExit("Tool paths must be regular files inside data/tools")
(Path(os.environ["RUNNER_TEMP"]) / "tool-files.json").write_text(json.dumps(files))

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Build pr-check
run: cargo build --locked --release --manifest-path ci/Cargo.toml -p pr-check

- name: Run pr-check without comment permissions
env:
GITHUB_TOKEN: ${{ github.token }}
COMMENT_OUTPUT_FILE: pr-check-output/comment.md
shell: python
run: |
import json
import os
from pathlib import Path
import subprocess

files = json.loads((Path(os.environ["RUNNER_TEMP"]) / "tool-files.json").read_text())
Path("pr-check-output").mkdir(exist_ok=True)
Path(os.environ["COMMENT_OUTPUT_FILE"]).write_text(
"The PR checker could not finish. Please inspect the PR Check workflow logs.\n"
)
result = subprocess.run(["ci/target/release/pr-check", *files])
raise SystemExit(result.returncode)

- name: Upload check feedback
if: always()
uses: actions/upload-artifact@v4
with:
name: pr-check-output
path: pr-check-output/comment.md
if-no-files-found: ignore
retention-days: 7

- name: Explain manual results
if: always() && github.event_name == 'workflow_dispatch'
run: |
echo 'Manual checks are read-only. Download pr-check-output for the report; no PR comment is posted because workflow_dispatch does not provide an associated PR.' >> "$GITHUB_STEP_SUMMARY"
119 changes: 119 additions & 0 deletions .github/workflows/pr-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: PR Check Comment

on:
workflow_run:
workflows: ["CI", "PR Check"]
types: [completed]

permissions:
actions: read
pull-requests: write

jobs:
comment:
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.repository.id == github.event.repository.id
runs-on: ubuntu-latest
steps:
# No checkout: artifacts are untrusted text, never code or comment targets.
- name: Resolve associated PR
id: target
uses: actions/github-script@v7
with:
script: |
const run = context.payload.workflow_run;
if (!run.head_repository?.id || !run.head_sha) {
core.notice('Missing run head identity. Skipping comment.');
return;
}
const matchesRun = pr =>
pr.state === 'open' &&
pr.base?.repo?.id === context.payload.repository.id &&
pr.head?.sha === run.head_sha &&
pr.head?.repo?.id === run.head_repository.id;
let candidates;
if (run.pull_requests?.length) {
candidates = await Promise.all(run.pull_requests.map(async pr => {
const {data} = await github.rest.pulls.get({
...context.repo, pull_number: pr.number
});
return data;
}));
} else {
// Fork runs can omit pull_requests. Only GitHub's API may supply
// fallback targets; a shared commit alone does not identify a fork.
candidates = await github.paginate(
github.rest.repos.listPullRequestsAssociatedWithCommit, {
...context.repo, commit_sha: run.head_sha, per_page: 100
});
}
const prs = candidates.filter(matchesRun);
if (prs.length !== 1) {
core.notice('No unique open PR matches the run head and repositories. Skipping comment.');
return;
}
// Recheck current state after discovery, including API fallback results.
const {data: pr} = await github.rest.pulls.get({
...context.repo, pull_number: prs[0].number
});
if (!matchesRun(pr)) {
core.notice('PR no longer matches the run. Skipping comment.');
return;
}
core.setOutput('number', pr.number);

- name: Download feedback
if: steps.target.outputs.number != ''
uses: actions/download-artifact@v4
with:
# Unlike name, pattern succeeds with zero artifacts (normal for passing CI).
pattern: ${{ github.event.workflow_run.name == 'PR Check' && 'pr-check-output' || 'ci-*-output' }}
path: ${{ runner.temp }}/feedback
github-token: ${{ github.token }}
run-id: ${{ github.event.workflow_run.id }}

- name: Post or update feedback
if: steps.target.outputs.number != ''
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ steps.target.outputs.number }}
with:
script: |
const fs = require('fs');
const path = require('path');
const run = context.payload.workflow_run;
const names = run.name === 'PR Check'
? ['pr-check-output'] : ['ci-readme-output', 'ci-render-output'];
const feedback = names.map(name => ({
name, file: path.join(process.env.RUNNER_TEMP, 'feedback', name, 'comment.md')
})).filter(({file}) => fs.existsSync(file));
if (!feedback.length) {
core.notice('No feedback artifacts were produced. Nothing to comment.');
return;
}
const issue_number = Number(process.env.PR_NUMBER);
const comments = await github.paginate(github.rest.issues.listComments, {
...context.repo, issue_number, per_page: 100
});
for (const {name, file} of feedback) {
const stat = fs.lstatSync(file);
if (!stat.isFile() || stat.size > 60000) {
core.warning(`Ignoring invalid or oversized feedback from ${name}`);
continue;
}
const text = fs.readFileSync(file, 'utf8').trim();
if (!text) continue;
const marker = `<!-- dynamic-analysis-${name} -->`;
const body = `${marker}\n${text}\n\n[Workflow logs](${run.html_url})`;
const existing = comments.find(c =>
c.user.login === 'github-actions[bot]' && c.user.type === 'Bot' &&
c.body.startsWith(marker));
if (existing) {
await github.rest.issues.updateComment({
...context.repo, comment_id: existing.id, body
});
} else {
await github.rest.issues.createComment({
...context.repo, issue_number, body
});
}
}
25 changes: 20 additions & 5 deletions .github/workflows/render.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,33 @@ on:
push:
branches: [master]

permissions:
contents: write

concurrency:
group: render-master
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- uses: actions/checkout@v4
with:
ref: master

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Render list
run: make render

- uses: stefanzweifel/git-auto-commit-action@v4.1.2
env:
GITHUB_TOKEN: ${{ github.token }}

- uses: stefanzweifel/git-auto-commit-action@v5
with:
branch: ${{ github.head_ref }}
branch: master
file_pattern: README.md data/api
commit_message: Commit list
commit_user_name: Analysis Tools Bot
commit_user_email: bot@analysis-tools.dev
Expand Down
Loading
Loading