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
121 changes: 79 additions & 42 deletions .github/workflows/llgo-binary-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,35 +51,31 @@ permissions:
contents: read
actions: read

concurrency:
# Keep benchmark builds in their own queue. The Pages-only workflow has a
# separate queue; sharing one group would let a newer pending Pages run
# replace a pending binary-size run even with cancel-in-progress disabled.
group: llgo-binary-size-build
cancel-in-progress: false

jobs:
update-pin:
if: github.event_name == 'repository_dispatch'
permissions:
contents: write
actions: write
runs-on: ubuntu-24.04
concurrency:
# Pin updates may be coalesced because the file only records the latest
# LLGo main revision. The benchmark job below is not part of this queue:
Comment on lines 57 to +62

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P3] Untrusted payload validated after write-scoped checkout

The update-pin job runs with contents: write and performs actions/checkout@v4 (persisting a write-scoped token) before the source_repository/llgo_repository/llgo_commit allow-listing and SHA validation run in the later step. Validation does gate the URL/git usage, so this is defense-in-depth rather than exploitable — and anyone able to send a repository_dispatch already holds a write token. Still, validating in a minimal-permission gating job that the privileged jobs needs: would ensure untrusted payloads never reach a contents: write context. The same pattern applies to the binary-size job's dispatch handling.

# every distinct dispatched LLGo commit still gets its own result.
group: llgo-binary-size-pin
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
with:
# A dispatch can wait behind a long binary-size build. Start from
# the current branch tip instead of the SHA captured when the event
# was created.
# A coalesced pin update can start after main has advanced. Always
# rebuild its one-file commit from the current branch tip.
ref: main
fetch-depth: 0

- name: Update the committed LLGo pin and start the build
- name: Update the committed LLGo pin
env:
DISPATCH_SOURCE_REPOSITORY: ${{ github.event.client_payload.source_repository }}
DISPATCH_LLGO_REPOSITORY: ${{ github.event.client_payload.llgo_repository }}
DISPATCH_LLGO_COMMIT: ${{ github.event.client_payload.llgo_commit }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
if [[ "$DISPATCH_SOURCE_REPOSITORY" != "xgo-dev/llgo" || \
Expand All @@ -92,6 +88,18 @@ jobs:
exit 1
fi

# A newer notification may replace an older pending pin update. Pin
# the current LLGo main tip so a delayed notification cannot move
# the committed default backwards.
target_commit="$(git ls-remote \
"https://github.com/${DISPATCH_LLGO_REPOSITORY}.git" \
refs/heads/main | awk 'NR == 1 { print $1 }')"
if [[ ! "$target_commit" =~ ^[0-9a-f]{40}$ ]]; then
echo "could not resolve ${DISPATCH_LLGO_REPOSITORY} main" >&2
exit 1
fi
echo "Pinning current LLGo main $target_commit (notification requested $DISPATCH_LLGO_COMMIT)"

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

Expand All @@ -104,16 +112,16 @@ jobs:
git checkout --detach origin/main

current_commit="$(awk -F= '$1 == "LLGO_COMMIT" { print $2 }' ci/llgo-size/llgo-version.env)"
if [[ "$current_commit" == "$DISPATCH_LLGO_COMMIT" ]]; then
echo "LLGo pin is already $DISPATCH_LLGO_COMMIT"
if [[ "$current_commit" == "$target_commit" ]]; then
echo "LLGo pin is already $target_commit"
pin_ready=true
break
fi

sed -i "s/^LLGO_COMMIT=.*/LLGO_COMMIT=${DISPATCH_LLGO_COMMIT}/" \
sed -i "s/^LLGO_COMMIT=.*/LLGO_COMMIT=${target_commit}/" \
ci/llgo-size/llgo-version.env
git add ci/llgo-size/llgo-version.env
git commit -m "ci: pin LLGo ${DISPATCH_LLGO_COMMIT:0:12}"
git commit -m "ci: pin LLGo ${target_commit:0:12}"
if git push origin HEAD:main; then
pin_ready=true
break
Expand All @@ -132,20 +140,6 @@ jobs:
exit 1
fi

# Dispatch even when the pin was already current. A retry may be
# recovering from a prior run that pushed the pin but failed before
# it could start the actual binary-size build.
payload="$(jq -cn \
--arg ref main \
--arg llgo_commit "$DISPATCH_LLGO_COMMIT" \
'{ref: $ref, inputs: {llgo_commit: $llgo_commit}}')"
curl --fail-with-body --location --request POST \
--header 'Accept: application/vnd.github+json' \
--header "Authorization: Bearer ${GH_TOKEN}" \
--header 'X-GitHub-Api-Version: 2022-11-28' \
"https://api.github.com/repos/${GITHUB_REPOSITORY}/actions/workflows/llgo-binary-size.yml/dispatches" \
--data "$payload"

pr-scope:
if: github.event_name == 'pull_request'
permissions:
Expand Down Expand Up @@ -186,26 +180,49 @@ jobs:

binary-size:
if: >-
always() && github.event_name != 'repository_dispatch' &&
(github.event_name != 'pull_request' || needs.pr-scope.outputs.full_matrix == 'true')
always() &&
(github.event_name != 'pull_request' || needs.pr-scope.outputs.full_matrix == 'true') &&
!(github.event_name == 'push' && github.actor == 'github-actions[bot]' &&
startsWith(github.event.head_commit.message, 'ci: pin LLGo '))
needs: pr-scope
permissions:
contents: write
runs-on: ubuntu-24.04
timeout-minutes: 120
concurrency:
# Different LLGo revisions never share a pending slot. Duplicate
# notifications for the same revision may be coalesced safely because
# published history is keyed by the full LLGo commit.
group: llgo-binary-size-${{ github.event.client_payload.llgo_commit || inputs.llgo_commit || github.sha }}
cancel-in-progress: false
steps:
- uses: actions/checkout@v4

- name: Read pinned toolchain versions
env:
DISPATCH_SOURCE_REPOSITORY: ${{ github.event.client_payload.source_repository }}
DISPATCH_LLGO_REPOSITORY: ${{ github.event.client_payload.llgo_repository }}
DISPATCH_LLGO_COMMIT: ${{ github.event.client_payload.llgo_commit }}
MANUAL_LLGO_COMMIT: ${{ inputs.llgo_commit }}
run: |
set -euo pipefail
set -a
source ci/llgo-size/llgo-version.env
set +a

if [[ -n "$MANUAL_LLGO_COMMIT" ]]; then
if [[ "$GITHUB_EVENT_NAME" == "repository_dispatch" ]]; then
if [[ "$DISPATCH_SOURCE_REPOSITORY" != "xgo-dev/llgo" || \
"$DISPATCH_LLGO_REPOSITORY" != "xgo-dev/llgo" ]]; then
echo "refusing binary-size dispatch from ${DISPATCH_SOURCE_REPOSITORY:-unknown}" >&2
exit 1
fi
if [[ ! "$DISPATCH_LLGO_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
echo "invalid dispatched LLGo commit: ${DISPATCH_LLGO_COMMIT:-missing}" >&2
exit 1
fi
LLGO_REPOSITORY="$DISPATCH_LLGO_REPOSITORY"
LLGO_COMMIT="$DISPATCH_LLGO_COMMIT"
elif [[ -n "$MANUAL_LLGO_COMMIT" ]]; then
if [[ ! "$MANUAL_LLGO_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
echo "manual LLGo commit must be a full 40-character SHA" >&2
exit 1
Expand Down Expand Up @@ -263,6 +280,13 @@ jobs:
git -C .ci/llgo fetch --progress --no-tags --depth=1 origin "$LLGO_COMMIT"
time_command "Check out pinned LLGo commit" \
git -C .ci/llgo checkout --detach "$LLGO_COMMIT"
git -C .ci/llgo fetch --no-tags origin main
git -C .ci/llgo rev-list --first-parent --reverse origin/main > .ci/llgo-main-history.txt
llgo_main_index="$(awk -v commit="$LLGO_COMMIT" '$0 == commit { print NR; exit }' \
.ci/llgo-main-history.txt)"
llgo_committed_at="$(git -C .ci/llgo show -s --format=%cI "$LLGO_COMMIT")"
printf 'LLGO_MAIN_INDEX=%s\n' "$llgo_main_index" >> "$GITHUB_ENV"
printf 'LLGO_COMMITTED_AT=%s\n' "$llgo_committed_at" >> "$GITHUB_ENV"
- name: Build LLGo and the LTO plugin
run: |
set -euo pipefail
Expand Down Expand Up @@ -335,14 +359,27 @@ jobs:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
pages_dir="$GITHUB_WORKSPACE/.ci/pages"
bash ci/llgo-size/prepare-pages-branch.sh \
"$pages_dir" \
"https://x-access-token:${PAGES_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
ci/llgo-size/publish.sh \
.ci/bent-run/results \
"$pages_dir" \
ci/llgo-size/site
published=false
for attempt in 1 2 3 4; do
pages_dir="$GITHUB_WORKSPACE/.ci/pages-$attempt"
bash ci/llgo-size/prepare-pages-branch.sh \
"$pages_dir" \
"https://x-access-token:${PAGES_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
if ci/llgo-size/publish.sh \
.ci/bent-run/results \
"$pages_dir" \
ci/llgo-size/site \
.ci/llgo-main-history.txt; then
published=true
break
fi
echo "Pages advanced during publication; retrying from its latest tip"
sleep $((attempt * 2))
done
if [[ "$published" != true ]]; then
echo "failed to publish binary-size history after 4 attempts" >&2
exit 1
fi
deploy-pages:
if: >-
always() && github.ref == 'refs/heads/main' &&
Expand Down
25 changes: 14 additions & 11 deletions ci/llgo-size/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ and a full-width separator marks the next benchmark. The row set is the union
of every published run, so a historical benchmark remains visible with `—` in
commit columns where it was not produced.

The `llgo-main-updated` repository-dispatch event from `xgo-dev/llgo` first
updates `LLGO_COMMIT` on the benchmarks `main` branch, then explicitly starts a
`workflow_dispatch` build with that complete SHA. This makes the version-file
update and the published result one ordered operation without relying on a
`GITHUB_TOKEN`-created push to trigger another workflow. The exact commit is
retained in `results.json` and the Pages history, so every LLGo `main` update
has a comparable data point. GitHub requires the receiver workflow to be on
the benchmarks repository's default branch before it can receive this event.
The `llgo-main-updated` repository-dispatch event from `xgo-dev/llgo` starts a
binary-size build directly with the complete dispatched SHA. A separate,
coalescible job updates `LLGO_COMMIT` on the benchmarks `main` branch to the
current LLGo main tip; coalescing that bookkeeping cannot discard a benchmark
revision. Distinct LLGo commits use distinct build concurrency keys, so a burst
of merges may run in parallel but every LLGo `main` update retains a comparable
data point. GitHub requires the receiver workflow to be on the benchmarks
repository's default branch before it can receive this event.
The workflow sources `timing.sh` for its shared CI step timing output.
Bent schedules benchmark/configuration builds serially; each LLGo invocation
uses the compiler's own package-level parallelism.
Expand All @@ -81,9 +81,12 @@ separate `llgo-binary-size-pages.yml` workflow. That path publishes the updated
site directly without rebuilding benchmarks, and its publication jobs are
restricted to `main`; pull-request builds cannot publish Pages.

The benchmark and page-only workflows use separate concurrency queues. A page
refresh can therefore wait independently without replacing a pending
binary-size run.
The benchmark and page-only workflows use separate concurrency keys. Pages
publication retries from the latest `pages` tip if parallel benchmark runs
finish together. The index records each result's position on LLGo's first-parent
`main` history and displays commits in that order rather than build completion
order; the dashboard opens on the newest page while keeping its columns oldest
to newest.

Pull requests that change the committed LLGo version, Bent, the LLGo-size
benchmark/configuration files, or the suite definitions used by those cases
Expand Down
35 changes: 35 additions & 0 deletions ci/llgo-size/enrich_pull_requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,34 @@ def repository_from_result(run, data_dir):
return str(result_document(run, data_dir).get("run", {}).get("llgoRepository", ""))


def load_main_history(path):
if path is None:
return []
with path.open(encoding="utf-8") as history_file:
commits = [line.strip().lower() for line in history_file if line.strip()]
invalid = [commit for commit in commits if not COMMIT_RE.fullmatch(commit)]
if invalid:
raise ValueError("invalid LLGo main commit in history: " + repr(invalid[0]))
return commits


def order_runs(index, main_history):
positions = {commit: position for position, commit in enumerate(main_history, start=1)}
for run in index.get("runs", []):
commit = str(run.get("llgoCommit", "")).lower()
if commit in positions:
run["llgoMainIndex"] = positions[commit]
Comment on lines +126 to +129

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P2] order_runs never clears a stale llgoMainIndex

order_runs only assigns llgoMainIndex when a run's commit is found in the freshly loaded --main-history; it never clears an existing value. A run persists its own build-time llgoMainIndex (baked into results.json by report.sh and copied into the index by publish.sh), so if that commit later drops off the current first-parent main history (history rewrite) or --main-history is omitted, the run keeps a potentially outdated position and the sort trusts it. In practice first-parent history is stable so this is low-likelihood, but recomputing/clearing the field for every run would make the freshly loaded history the single source of truth.


def order_key(run):
position = run.get("llgoMainIndex")
if isinstance(position, int) and not isinstance(position, bool):
return (0, position, "", str(run.get("key", "")))
committed_at = str(run.get("llgoCommittedAt") or run.get("createdAt") or "")
return (1, 0, committed_at, str(run.get("key", "")))

index.setdefault("runs", []).sort(key=order_key)
Comment on lines +131 to +138

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[P3] Runs absent from main history always sort last, ignoring commit date

In order_key, any run with an integer llgoMainIndex is placed in group 0; every run without one goes to group 1. A run whose LLGo commit is not on first-parent main (e.g. a manual workflow_dispatch of an arbitrary/branch commit) is therefore forced to the very end of the timeline regardless of its llgoCommittedAt. Given the oldest-first display this makes such a run appear as the newest entry, so latestRun() picks it as the default environment/page. This may be intended (main history is authoritative), but the consequence is non-obvious — a short comment noting the intent, or reconsidering the fallback, would help.



def legacy_wall_times(run, document, data_dir):
native_path = document.get("native", {}).get("buildTimes")
path = result_path(run, data_dir)
Expand Down Expand Up @@ -221,6 +249,11 @@ def parse_args(argv):
"--api-url",
default=os.environ.get("GITHUB_API_URL", "https://api.github.com"),
)
parser.add_argument(
"--main-history",
type=Path,
help="first-parent LLGo main commits, oldest first",
)
return parser.parse_args(argv)


Expand All @@ -232,6 +265,8 @@ def main(argv=None):
index = json.load(index_file)
token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN", "")

order_runs(index, load_main_history(args.main_history))

def lookup(repository, commit):
return github_pull_request_lookup(repository, commit, args.api_url, token)

Expand Down
12 changes: 9 additions & 3 deletions ci/llgo-size/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
results_dir="$1"
pages_dir="$2"
site_dir="$3"
main_history="${4:-}"
if [[ -z "$results_dir" || -z "$pages_dir" || -z "$site_dir" ]]; then
echo "usage: publish.sh RESULTS_DIR PAGES_DIR SITE_DIR" >&2
echo "usage: publish.sh RESULTS_DIR PAGES_DIR SITE_DIR [LLGO_MAIN_HISTORY]" >&2
exit 2
fi

Expand Down Expand Up @@ -133,6 +134,8 @@ for path in glob.glob(os.path.join(data_dir, "runs", "*", "results.json")):
"ref": run.get("ref", ""),
"llgoRepository": run.get("llgoRepository", ""),
"llgoCommit": run.get("llgoCommit", ""),
"llgoMainIndex": run.get("llgoMainIndex"),
"llgoCommittedAt": run.get("llgoCommittedAt", ""),
"goVersion": run.get("goVersion", ""),
"llvmVersion": run.get("llvmVersion", ""),
"workflowUrl": run.get("workflowUrl", ""),
Expand All @@ -149,7 +152,6 @@ for path in glob.glob(os.path.join(data_dir, "runs", "*", "results.json")):
if field in previous:
item[field] = previous[field]
runs.append(item)
runs.sort(key=lambda item: item["createdAt"], reverse=True)

index = {
"schemaVersion": 1,
Expand All @@ -160,7 +162,11 @@ with open(runs_index_path, "w", encoding="utf-8") as f:
json.dump(index, f, indent=2)
f.write("\n")
PY
python3 "$script_dir/enrich_pull_requests.py" "$pages_dir/data/index.json"
enrich_args=("$pages_dir/data/index.json")
if [[ -n "$main_history" && -s "$main_history" ]]; then
enrich_args+=(--main-history "$main_history")
fi
python3 "$script_dir/enrich_pull_requests.py" "${enrich_args[@]}"

git -C "$pages_dir" config user.name "github-actions[bot]"
git -C "$pages_dir" config user.email "41898282+github-actions[bot]@users.noreply.github.com"
Expand Down
2 changes: 2 additions & 0 deletions ci/llgo-size/report.sh
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ run = {
"ref": env("LLGO_SIZE_REF", "GITHUB_REF_NAME"),
"llgoRepository": os.environ.get("LLGO_REPOSITORY", ""),
"llgoCommit": os.environ.get("LLGO_COMMIT", ""),
"llgoMainIndex": number("LLGO_MAIN_INDEX", ""),
"llgoCommittedAt": os.environ.get("LLGO_COMMITTED_AT", ""),
"goVersion": os.environ.get("GO_VERSION", ""),
"llvmVersion": os.environ.get("LLVM_VERSION", ""),
"event": env("LLGO_SIZE_EVENT", "GITHUB_EVENT_NAME"),
Expand Down
Loading
Loading