-
Notifications
You must be signed in to change notification settings - Fork 15
ci: build a pull request's binaries on a /build comment #204
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,245 @@ | ||
| # Binaries built from a pull request, for testing a change without releasing | ||
| # one. Comment on the pull request: | ||
| # | ||
| # /build every target | ||
| # /build x86_64-linux one target | ||
| # /build x86_64-linux aarch64-linux a few | ||
| # | ||
| # The build finishes with a comment linking each artifact and its b3sum. | ||
| name: soar pr build | ||
| concurrency: | ||
| group: "${{ github.workflow }}-${{ github.event.issue.number }}" | ||
| cancel-in-progress: true | ||
|
|
||
| on: | ||
| issue_comment: | ||
| types: [created] | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| request: | ||
| name: Read the request | ||
| # A build runs the pull request's own code, including its build scripts, so | ||
| # asking for one is as good as pushing to a branch here. Only someone who | ||
| # could do that anyway gets to ask. | ||
| if: >- | ||
| github.event.issue.pull_request && | ||
| startsWith(github.event.comment.body, '/build') && | ||
| contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association) | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| issues: write | ||
| pull-requests: write | ||
| outputs: | ||
| matrix: ${{ steps.targets.outputs.matrix }} | ||
| sha: ${{ steps.head.outputs.sha }} | ||
| short_sha: ${{ steps.head.outputs.short_sha }} | ||
| steps: | ||
| - name: Choose the targets | ||
| id: targets | ||
| env: | ||
| # Read from the environment rather than interpolated into the script, | ||
| # which would run whatever the comment happens to contain. | ||
| BODY: ${{ github.event.comment.body }} | ||
| run: | | ||
| all=(aarch64-linux riscv64-linux x86_64-linux) | ||
| triple_for() { | ||
| case "$1" in | ||
| aarch64-linux) echo aarch64-unknown-linux-musl ;; | ||
| riscv64-linux) echo riscv64gc-unknown-linux-musl ;; | ||
| x86_64-linux) echo x86_64-unknown-linux-musl ;; | ||
| *) return 1 ;; | ||
| esac | ||
| } | ||
|
|
||
| read -ra words <<< "$(printf '%s' "$BODY" | tr -d '\r' | head -n 1)" | ||
| # `/buildsomething` is somebody else's command, not a bare `/build`. | ||
| if [ "${words[0]}" != "/build" ]; then | ||
| echo "not a build request; ignoring" | ||
| exit 0 | ||
| fi | ||
|
|
||
| asked=("${words[@]:1}") | ||
| if [ ${#asked[@]} -eq 0 ]; then | ||
| asked=("${all[@]}") | ||
| fi | ||
|
|
||
| entries=() | ||
| chosen=() | ||
| for name in "${asked[@]}"; do | ||
| if ! triple=$(triple_for "$name"); then | ||
| echo "unknown=$name" >> "$GITHUB_OUTPUT" | ||
| exit 0 | ||
| fi | ||
| # A name asked for twice would build twice and collide on upload. | ||
| case " ${chosen[*]} " in | ||
| *" $name "*) continue ;; | ||
| esac | ||
| chosen+=("$name") | ||
| entries+=("{\"NAME\":\"$name\",\"TARGET\":\"$triple\"}") | ||
| done | ||
|
|
||
| printf 'matrix=[%s]\n' "$(IFS=,; echo "${entries[*]}")" >> "$GITHUB_OUTPUT" | ||
| echo "building: ${chosen[*]}" | ||
|
|
||
| - name: Say which targets exist | ||
| if: ${{ steps.targets.outputs.unknown }} | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| PR: ${{ github.event.issue.number }} | ||
| UNKNOWN: ${{ steps.targets.outputs.unknown }} | ||
| run: | | ||
| gh pr comment "$PR" --body "$(printf 'No target named `%s`. Pick from `aarch64-linux`, `riscv64-linux`, `x86_64-linux`, or say `/build` on its own to build all three.' "$UNKNOWN")" | ||
| exit 1 | ||
|
|
||
| - name: Acknowledge the comment | ||
| if: ${{ steps.targets.outputs.matrix }} | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| COMMENT: ${{ github.event.comment.url }} | ||
| run: gh api --silent -X POST "$COMMENT/reactions" -f content=eyes | ||
|
|
||
| - name: Resolve the head commit | ||
| id: head | ||
| if: ${{ steps.targets.outputs.matrix }} | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| REPO: ${{ github.repository }} | ||
| PR: ${{ github.event.issue.number }} | ||
| run: | | ||
| sha=$(gh api "repos/$REPO/pulls/$PR" --jq .head.sha) | ||
| echo "sha=$sha" >> "$GITHUB_OUTPUT" | ||
| echo "short_sha=${sha:0:7}" >> "$GITHUB_OUTPUT" | ||
|
|
||
| build: | ||
| name: Build ${{ matrix.build.NAME }} | ||
| needs: request | ||
| if: ${{ needs.request.outputs.matrix }} | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| build: ${{ fromJSON(needs.request.outputs.matrix) }} | ||
| steps: | ||
| - name: Checkout the pull request | ||
| uses: actions/checkout@v5 | ||
| with: | ||
| # The commit itself, so the binary matches what the comment reports | ||
| # even if the pull request is pushed to while this runs. | ||
| ref: ${{ needs.request.outputs.sha }} | ||
| persist-credentials: false | ||
|
|
||
| - name: Install dependencies | ||
| shell: bash | ||
| run: | | ||
| sudo apt update -y | ||
| sudo apt install b3sum findutils file -y | ||
|
|
||
| - name: Install Rust toolchain | ||
| uses: dtolnay/rust-toolchain@stable | ||
| with: | ||
| targets: ${{ matrix.build.TARGET }} | ||
|
|
||
| # The released binary, rather than a build from git that spends minutes | ||
| # compiling before anything of ours does. | ||
| - name: Install Cross | ||
| uses: taiki-e/install-action@v2 | ||
| with: | ||
| tool: cross@0.2.5 | ||
|
|
||
| - name: Build | ||
| env: | ||
| RUSTFLAGS: "-C target-feature=+crt-static \ | ||
| -C link-self-contained=yes \ | ||
| -C link-arg=-Wl,--build-id=none" | ||
| # A released cross asks for images tagged with its own version, and | ||
| # 0.2.5 is old enough that the riscv64 musl image was never published | ||
| # under it. Naming the tag is what keeps that target buildable, and it | ||
| # is the same `main` that a cross built from git resolves to, so these | ||
| # binaries come out of the images the releases are built in. | ||
| CROSS_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_IMAGE: ghcr.io/cross-rs/aarch64-unknown-linux-musl:main | ||
| CROSS_TARGET_RISCV64GC_UNKNOWN_LINUX_MUSL_IMAGE: ghcr.io/cross-rs/riscv64gc-unknown-linux-musl:main | ||
| CROSS_TARGET_X86_64_UNKNOWN_LINUX_MUSL_IMAGE: ghcr.io/cross-rs/x86_64-unknown-linux-musl:main | ||
| run: cross build --release -F self --locked --target "${{ matrix.build.TARGET }}" --jobs="$(($(nproc)+1))" --verbose | ||
|
|
||
| - name: Create build artifacts | ||
| env: | ||
| ARTIFACT: "soar-${{ matrix.build.NAME }}" | ||
| shell: bash | ||
| run: | | ||
| cp "target/${{ matrix.build.TARGET }}/release/soar" "${ARTIFACT}" | ||
| b3sum "${ARTIFACT}" > "${ARTIFACT}.b3sum" | ||
| printf "\nFile: ${ARTIFACT}\n Type: $(file -b "${ARTIFACT}")\n B3sum: $(cut -d' ' -f1 "${ARTIFACT}.b3sum")\n Size: $(du -bh "${ARTIFACT}" | cut -f1)\n" | ||
|
|
||
| - name: Upload artifact | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: soar-${{ matrix.build.NAME }}-${{ needs.request.outputs.short_sha }} | ||
| path: soar-${{ matrix.build.NAME }}* | ||
|
|
||
| report: | ||
| name: Report back | ||
| needs: [request, build] | ||
| if: ${{ !cancelled() && needs.request.outputs.matrix }} | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| actions: read | ||
| pull-requests: write | ||
| steps: | ||
| # Only for the checksums. A build that produced nothing leaves this with | ||
| # nothing to fetch, which is what the comment is for. | ||
| - name: Download the artifacts | ||
| continue-on-error: true | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| pattern: soar-* | ||
| path: artifacts | ||
|
|
||
| - name: Comment with the result | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| PR: ${{ github.event.issue.number }} | ||
| REPO: ${{ github.repository }} | ||
| RUN: ${{ github.run_id }} | ||
| SERVER: ${{ github.server_url }} | ||
| SHA: ${{ needs.request.outputs.sha }} | ||
| SHORT_SHA: ${{ needs.request.outputs.short_sha }} | ||
| RESULT: ${{ needs.build.result }} | ||
| run: | | ||
| run_url="$SERVER/$REPO/actions/runs/$RUN" | ||
| commit_url="$SERVER/$REPO/commit/$SHA" | ||
|
|
||
| # Each target builds on its own, so the run as a whole failing does | ||
| # not mean every target did. The artifacts that exist are listed | ||
| # either way. | ||
| gh api "repos/$REPO/actions/runs/$RUN/artifacts" \ | ||
| --jq '.artifacts[] | [.name, .id] | @tsv' > built.tsv | ||
|
|
||
| { | ||
| if [ ! -s built.tsv ]; then | ||
| printf 'Build of [`%s`](%s) %s, with nothing to show for it. See the [run](%s).\n' \ | ||
| "$SHORT_SHA" "$commit_url" "$RESULT" "$run_url" | ||
| else | ||
| printf 'Built [`%s`](%s):\n\n' "$SHORT_SHA" "$commit_url" | ||
| printf '| target | artifact | b3sum |\n| --- | --- | --- |\n' | ||
| while IFS=$'\t' read -r name id; do | ||
| target=${name%-*} | ||
| target=${target#soar-} | ||
| sum=$(cut -d' ' -f1 "artifacts/$name/soar-$target.b3sum" 2>/dev/null || true) | ||
| printf '| `%s` | [%s](%s/artifacts/%s) | `%s` |\n' \ | ||
| "$target" "$name" "$run_url" "$id" "${sum:-unknown}" | ||
| done < built.tsv | ||
|
|
||
| if [ "$RESULT" != "success" ]; then | ||
| printf '\nSome targets did not build. See the [run](%s).\n' "$run_url" | ||
| fi | ||
|
|
||
| printf '\nDownloading an artifact needs a GitHub login, and it arrives as a zip. ' | ||
| printf 'They expire with this repository'"'"'s retention setting.\n' | ||
| fi | ||
| } > comment.md | ||
|
|
||
| gh pr comment "$PR" --body-file comment.md | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: pkgforge/soar
Length of output: 27964
🌐 Web query:
GitHub Actions concurrency matrix jobs same concurrency group cancel-in-progress behavior matrix💡 Result:
In GitHub Actions, the behavior of concurrency groups with matrix jobs depends on where the concurrency key is defined and how the group is scoped. When
concurrencyis defined at the workflow level, it governs the entire workflow run, including all jobs within that run [1][2]. If a new workflow run is triggered that shares the same concurrency group andcancel-in-progressis set to true, the entire previous workflow run is canceled, which includes all in-progress matrix jobs simultaneously [1][2]. Whenconcurrencyis defined at the job level, it applies only to that specific job [1][2]. If a matrix job is defined with aconcurrencygroup, each matrix leg (each unique combination of the matrix) is treated as an individual job instance [3]. If these legs share the same concurrency group (e.g., if the group key is static or identical across all matrix variations), they will effectively be serialized—only one will run at a time, andcancel-in-progress: truewill cause a new matrix leg to cancel an existing one in that same group [1][2]. Important implementation notes: 1. Scope Limitations: Workflow-level concurrency cannot use thematrixcontext because the matrix has not been evaluated at the workflow scope [4]. If you want to use matrix values to define a concurrency group, you must apply theconcurrencykey at the individual job level [1][4]. 2. Distinguishing Cancellation: It is important not to confuse concurrency cancellation withstrategy.fail-fast[5][6]. By default,strategy.fail-fast: truecauses all other matrix legs to be canceled immediately if any single leg fails [5][3]. This is independent of yourconcurrencysettings [6]. To allow all matrix legs to finish regardless of failures, you should setstrategy.fail-fast: false[5][6]. In summary, a matrix is simply a collection of jobs; a concurrency group acts on whatever jobs (or full workflows) are assigned to it. If multiple matrix legs fall into the same concurrency group, they will compete for that slot [1][2].Citations:
🌐 Web query:
site:docs.github.com GitHub Actions jobs.<job_id>.concurrency matrix context expression💡 Result:
In GitHub Actions, you can use the matrix context expression within the jobs.<job_id>.concurrency key to create dynamic concurrency groups [1][2][3]. When defining concurrency at the job level (jobs.<job_id>.concurrency), GitHub allows the use of expressions that include the matrix context [4][1][2]. This is particularly useful for controlling concurrency on a per-matrix-combination basis, such as ensuring that only one job runs for a specific operating system or environment configuration within a matrix, rather than globally for the entire job [1][3]. For example: jobs: example-job: strategy: matrix: os: [ubuntu-latest, windows-latest] concurrency: group: ${{ github.workflow }}-${{ matrix.os }} cancel-in-progress: true runs-on: ${{ matrix.os }}... This configuration ensures that concurrency is scoped to the specific matrix variant (e.g., separate groups for ubuntu-latest and windows-latest) [1]. Important distinction: While the job-level concurrency key supports the matrix context, top-level (workflow-level) concurrency does not, as it is evaluated before matrix strategies are expanded [5][6]. Always ensure you are defining your concurrency key under jobs.<job_id>.concurrency to access matrix variables [4][1].
Citations:
🏁 Script executed:
Repository: pkgforge/soar
Length of output: 500
Limit cancellation to validated build jobs.
Move
concurrencytobuildand includematrix.build.NAMEin its group. Otherwise, matrix legs share one group and can cancel each other.Add
needs.build.result != 'cancelled'to thereportcondition.🤖 Prompt for AI Agents