Skip to content

Commit 7ec86b2

Browse files
committed
feat: add gha
Signed-off-by: Matthias Riegler <[email protected]>
1 parent 6304fdc commit 7ec86b2

File tree

2 files changed

+165
-47
lines changed

2 files changed

+165
-47
lines changed

README.md

+54-47
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# mq-lease-service
2-
> A priority mutex with stabilisation window and TTLs, designed to work with the Github MergeQueue accessing a shared resource.
2+
> Add a funnel for shared resources on the Github Merge Queue.
3+
4+
If you want to use the github merge-queue feature and have a shared resource that can only be accessed by one process at a time (e.g. deploying a staging environment and running tests), this service/action is for you.
5+
It allows actions running as part of a merge group to get a mutex/lease on a shared resource, ensuring that only one action can access the resource at a time. Here, it takes in the priority of the action based on the number of commits ahead of the base branch and ensures that the highest priority action gets the lease.
6+
After releasign the mutex/lease (and by the nature of the merge queue combining commits), a positive outcome is propagated forward to action runs with a lower priority, or, on a failure, the lease is released and the next action with the highest priority gets the lease.
37

48
## Contributing
59

@@ -23,6 +27,53 @@ make run-server # to run the server
2327

2428
## Components
2529

30+
### Github Action
31+
The Github Action component of this repo interacts with the LeaseProvider and determines the priority of each run based on the commits ahead of the base.
32+
It has two stages:
33+
1. Acquiring a lease
34+
2. Releasing a lease when (1) has been successful, reporting the Github action job status
35+
36+
An example workflow can look like this:
37+
```yaml
38+
on:
39+
# Trigger on PRs; here we just skip
40+
pull_request:
41+
branches: [ main ]
42+
# Trigger on the merge group event
43+
merge_group:
44+
branches: [ main ]
45+
types: [ checks_requested ]
46+
47+
jobs:
48+
access-shared-resource:
49+
runs-on: ubuntu-latest
50+
if: ${{ github.event_name == 'merge_group' }}
51+
steps:
52+
- name: Checkout Plugin Repository
53+
uses: actions/checkout@v3
54+
- name: acquire lease
55+
id: acquire_lease
56+
uses: ankorstore/mq-lease-service@main
57+
with:
58+
endpoint: https://your.lease.service.com
59+
# auth: "" Optional: Authorization header value
60+
61+
- name: sleep
62+
# Only perform github actions when this run acquired the lease
63+
if: ${{ steps.acquire_lease.outputs.result == 'acquired' }}
64+
run: |
65+
echo "Do some stuff"
66+
67+
- name: release lease
68+
id: release
69+
uses: ankorstore/mq-lease-service@main
70+
if: always() # always run this step to ensure we don't leave the lease-service in a dirty state
71+
with:
72+
endpoint: https://your.lease.service.com
73+
release_with_status: ${{ job.status }}
74+
# auth: "" Optional: Authorization header value
75+
```
76+
2677
### LeaseProvider
2778
The LeaseProvider is a server that provides the ability to manage distributed leases among multiple github action runs, letting the highest priority run _win_ the lease. This process is helpful when there are multiple runs running that need access to a shared resource. It allows them to agree on the _winner_ of a race for the resource, and subsequently provide the _winner_ with a lease until it is released.
2879
Depending on the release status (success/failure), the lease is completed and confirmation is awaited or the request from the failing lease is discarded and the process restarts.
@@ -31,7 +82,7 @@ It exposes the following endpoints:
3182
- GET `/healthz` Kubernetes health endpoint
3283
- GET `/readyz` Kubernetes readiness endpoint
3384
- GET `/metrics` Prometheus metric endpoint
34-
- POST `/:owner/:repo/:baseRef/aquire` for aquiring a lease (poll until status is acquired or completed)
85+
- POST `/:owner/:repo/:baseRef/acquire` for aquiring a lease (poll until status is acquired or completed)
3586
- POST `/:owner/:repo/:baseRef/release` for releasing a lease (the winnder informs the LeaseProvider with the end result)
3687

3788
The payload and response (_LeaseRequest_) is encoded as JSON and follows this scheme:
@@ -46,7 +97,7 @@ The payload and response (_LeaseRequest_) is encoded as JSON and follows this sc
4697
Configuration options:
4798
- `--port` (8080)
4899
- `--stabilisation-window` (5m) - time to wait before giving out a lease without all expected PRs being in the merge queue
49-
- `--ttl` (30s) - time to wait before considering an aquire interest being stale
100+
- `--ttl` (30s) - time to wait before considering an acquire interest being stale
50101
- `--expected-build-count` (4) - number of parallel builds to be expected for a given merge group
51102

52103
#### STM of status transformations
@@ -183,47 +234,3 @@ sequenceDiagram
183234
```
184235

185236

186-
187-
### GithubAction
188-
The GithubAction component of this repo interacts with the LeaseProvider and determines the priority of each run based on the commits ahead of the base.
189-
It has two stages:
190-
1. Acquiring a lease
191-
2. Releasing a lease when (1) has been successful, reporting the Github action job status
192-
193-
An example workflow can look like this:
194-
```yaml
195-
on:
196-
# Trigger on PRs; here we just skip
197-
pull_request:
198-
branches: [ main ]
199-
# Trigger on the merge group event
200-
merge_group:
201-
branches: [ main ]
202-
types: [ checks_requested ]
203-
204-
jobs:
205-
access-shared-resource:
206-
runs-on: ubuntu-latest
207-
if: ${{ github.event_name == 'merge_group' }}
208-
steps:
209-
- name: Checkout Plugin Repository
210-
uses: actions/checkout@v3
211-
- name: Aquire lease
212-
id: acquire_lease
213-
uses: ankorstore/mq-lease-service@main
214-
with:
215-
# Endpoint to connect to
216-
endpoint: https://your.lease.service.com
217-
# **IMPORTANT** the job-status always has to be defined like this
218-
job_status: ${{ job.status }}
219-
# Optional: timeout_seconds (how long to wait before something is considered going wrong)
220-
# timeout_seconds: 7200
221-
# Optional: interval_seconds (the polling interval)
222-
# timeout_seconds: 15
223-
- name: sleep
224-
# Only perform github actions when this run acquired the lease
225-
if: ${{ steps.acquire_lease.outputs.result == 'acquired' }}
226-
run: |
227-
sleep 30
228-
# Note: there is no further action required, the lease will be released in a post hook of the `Aquire lease` action.
229-
```

action.yaml

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
name: MQ Lease Service
2+
description: "Interact with the mq-lease-service to have a priority-mutex/deduplication mechanism"
3+
inputs:
4+
lease_service_url:
5+
description: "URL of the lease service"
6+
required: true
7+
release_with_status:
8+
description: "If set, release a previously acquired lease with the given status"
9+
required: false
10+
default: ""
11+
auth:
12+
description: "Authorization header to use for the lease service"
13+
required: false
14+
default: ""
15+
outputs:
16+
status:
17+
value: "${{ steps.acquire.outputs.status }}"
18+
description: "Lease Status"
19+
stacked_pull_requests:
20+
value: "${{ steps.acquire.outputs.stacked_pull_requests }}"
21+
description: "Stacked Pull Requests"
22+
runs:
23+
using: "composite"
24+
steps:
25+
- name: retrieve status for merge group
26+
shell: bash
27+
id: status
28+
env:
29+
MERGE_GROUP_BASE_SHA: "${{ github.event.merge_group.base_sha }}"
30+
MERGE_GROUP_HEAD_SHA: "${{ github.event.merge_group.head_sha }}"
31+
MERGE_GROUP_BASE_REF: "${{ github.event.merge_group.base_ref }}"
32+
MERGE_GROUP_HEAD_REF: "${{ github.event.merge_group.head_ref }}"
33+
GH_TOKEN: ${{ github.token }}
34+
run: |
35+
set -eux
36+
# Determine merge group priority
37+
echo "merge_group_priority=$(
38+
gh api "/repos/${{ github.repository_owner }}/${{ github.event.repository.name }}/compare/${MERGE_GROUP_BASE_SHA}...${MERGE_GROUP_HEAD_SHA}" \
39+
| jq -r '.ahead_by'
40+
)" >> "$GITHUB_OUTPUT"
41+
42+
echo "merge_group_base_branch=${MERGE_GROUP_BASE_REF#refs/heads/}" >> "$GITHUB_OUTPUT"
43+
echo "merge_group_head_branch=${MERGE_GROUP_HEAD_REF#refs/heads/}" >> "$GITHUB_OUTPUT"
44+
45+
46+
# Try to acquire the lease
47+
- name: acquire
48+
id: acquire
49+
if: ${{ inputs.release_with_status == '' }}
50+
shell: bash
51+
env:
52+
LEASE_API_ENDPOINT: "${{ inputs.lease_service_url }}/${{ github.repository }}/${{ steps.status.outputs.merge_group_base_branch }}"
53+
AUTH_HEADER_VALUE: "${{ inputs.auth }}"
54+
HEAD_SHA: "${{ github.event.merge_group.head_sha }}"
55+
BASE_BRANCH: "${{ steps.status.outputs.merge_group_base_branch }}"
56+
HEAD_BRANCH: "${{ steps.status.outputs.merge_group_head_branch }}"
57+
PRIORITY: "${{ steps.status.outputs.merge_group_priority }}"
58+
run: |
59+
while true; do
60+
echo "[ ] Trying to acquire lease"
61+
resp=$(curl -H"Content-Type: json" -H"Authorization: ${AUTH_HEADER_VALUE}" -X POST -d "{\"head_sha\": \"${HEAD_SHA}\", \"head_ref\": \"${HEAD_BRANCH}\", \"priority\": ${PRIORITY}}" "{$LEASE_API_ENDPOINT}/acquire")
62+
if [[ $? -ne 0 ]]; then
63+
echo "[-] Failed to contact lease service (${resp}), retrying"
64+
sleep 5
65+
continue
66+
fi
67+
status=$(echo "$resp" | jq -r '.request.status')
68+
error=$(echo "$resp" | jq -r '.error')
69+
70+
if [[ "$error" != "null" ]]; then
71+
echo "[-] Unable to aquire lease: $error, retrying"
72+
sleep 15
73+
continue
74+
fi
75+
76+
if [[ "$status" == "pending" ]]; then
77+
echo "[+] Lease pending, retrying"
78+
sleep 15
79+
continue
80+
fi
81+
82+
echo "[+] Lease status: $status"
83+
84+
# Retrieve stacked pullrequest for better communication at later stages
85+
stacked_pull_requests=$(echo "$resp" | jq -c '.stacked_pull_requests')
86+
# Report to output
87+
echo "status=$status" >> "${GITHUB_OUTPUT}"
88+
echo "stacked_pull_requests=$stacked_pull_requests" >> "${GITHUB_OUTPUT}"
89+
break
90+
done
91+
92+
- name: release
93+
id: release
94+
if: ${{ inputs.release_with_status != '' }}
95+
shell: bash
96+
env:
97+
LEASE_API_ENDPOINT: "${{ inputs.lease_service_url }}/${{ github.repository }}/${{ steps.status.outputs.merge_group_base_branch }}"
98+
AUTH_HEADER_VALUE: "${{ inputs.auth }}"
99+
HEAD_SHA: "${{ github.event.merge_group.head_sha }}"
100+
BASE_BRANCH: "${{ steps.status.outputs.merge_group_base_branch }}"
101+
HEAD_BRANCH: "${{ steps.status.outputs.merge_group_head_branch }}"
102+
PRIORITY: "${{ steps.status.outputs.merge_group_priority }}"
103+
STATUS: "${{ inputs.release_with_status }}"
104+
run: |
105+
resp=$(curl -H"Content-Type: json" --fail-with-body -H"Authorization: ${AUTH_HEADER_VALUE}" -X POST -d "{\"head_sha\": \"${HEAD_SHA}\", \"head_ref\": \"${HEAD_BRANCH}\", \"priority\": ${PRIORITY}, \"status\": \"${STATUS}\"}" "${LEASE_API_ENDPOINT}/release")
106+
if [ $? -eq 0 ]; then
107+
echo "[+] Updated lease status: $RELEASE_WITH_STATUS"
108+
else
109+
echo "[!] Failed to update lease status - merge queue may be stuck"
110+
echo "$resp"
111+
fi

0 commit comments

Comments
 (0)