Without explicit timeouts, stuck jobs use the GitHub Actions default of 360 minutes (6 hours). This blocks the concurrency group and prevents new runs from starting. Set reasonable limits: - Build: 30 minutes (normally ~5 min) - E2E: 120 minutes (normally ~60 min) Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
306 lines
9.9 KiB
YAML
306 lines
9.9 KiB
YAML
name: Pull Request
|
||
|
||
env:
|
||
# TODO: unhardcode this
|
||
REGISTRY: iad.ocir.io/idyksih5sir9/cozystack
|
||
on:
|
||
pull_request:
|
||
types: [opened, synchronize, reopened]
|
||
|
||
# Cancel in‑flight runs for the same PR when a new push arrives.
|
||
concurrency:
|
||
group: pr-${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||
cancel-in-progress: true
|
||
|
||
jobs:
|
||
detect-changes:
|
||
name: Detect changes
|
||
runs-on: ubuntu-latest
|
||
outputs:
|
||
code: ${{ steps.filter.outputs.code }}
|
||
steps:
|
||
- uses: dorny/paths-filter@v3
|
||
id: filter
|
||
with:
|
||
filters: |
|
||
code:
|
||
- '!docs/**'
|
||
|
||
build:
|
||
name: Build
|
||
runs-on: [self-hosted]
|
||
timeout-minutes: 30
|
||
permissions:
|
||
contents: read
|
||
packages: write
|
||
|
||
needs: ["detect-changes"]
|
||
# Never run when the PR carries the "release" label or only docs changed.
|
||
if: |
|
||
needs.detect-changes.outputs.code == 'true'
|
||
&& !contains(github.event.pull_request.labels.*.name, 'release')
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
fetch-tags: true
|
||
|
||
- name: Run unit tests
|
||
run: make unit-tests
|
||
|
||
- name: Set up Docker config
|
||
run: |
|
||
if [ -d ~/.docker ]; then
|
||
cp -r ~/.docker "${{ runner.temp }}/.docker"
|
||
fi
|
||
|
||
- name: Login to GitHub Container Registry
|
||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||
uses: docker/login-action@v3
|
||
with:
|
||
username: ${{ secrets.OCIR_USER}}
|
||
password: ${{ secrets.OCIR_TOKEN }}
|
||
registry: iad.ocir.io
|
||
env:
|
||
DOCKER_CONFIG: ${{ runner.temp }}/.docker
|
||
|
||
- name: Build
|
||
run: make build
|
||
env:
|
||
DOCKER_CONFIG: ${{ runner.temp }}/.docker
|
||
|
||
- name: Build Talos image
|
||
run: make -C packages/core/talos talos-nocloud
|
||
|
||
- name: Save git diff as patch
|
||
if: "!contains(github.event.pull_request.labels.*.name, 'release')"
|
||
run: git diff HEAD > _out/assets/pr.patch
|
||
|
||
- name: Upload git diff patch
|
||
if: "!contains(github.event.pull_request.labels.*.name, 'release')"
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: pr-patch
|
||
path: _out/assets/pr.patch
|
||
|
||
- name: Upload Talos image
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: talos-image
|
||
path: _out/assets/nocloud-amd64.raw.xz
|
||
|
||
resolve_assets:
|
||
name: "Resolve assets"
|
||
runs-on: ubuntu-latest
|
||
if: contains(github.event.pull_request.labels.*.name, 'release')
|
||
outputs:
|
||
disk_id: ${{ steps.fetch_assets.outputs.disk_id }}
|
||
|
||
steps:
|
||
- name: Checkout code
|
||
if: contains(github.event.pull_request.labels.*.name, 'release')
|
||
uses: actions/checkout@v4
|
||
with:
|
||
fetch-depth: 0
|
||
fetch-tags: true
|
||
|
||
- name: Extract tag from PR branch (release PR)
|
||
if: contains(github.event.pull_request.labels.*.name, 'release')
|
||
id: get_tag
|
||
uses: actions/github-script@v7
|
||
with:
|
||
script: |
|
||
const branch = context.payload.pull_request.head.ref;
|
||
const m = branch.match(/^release-(\d+\.\d+\.\d+(?:[-\w\.]+)?)$/);
|
||
if (!m) {
|
||
core.setFailed(`❌ Branch '${branch}' does not match 'release-X.Y.Z[-suffix]'`);
|
||
return;
|
||
}
|
||
core.setOutput('tag', `v${m[1]}`);
|
||
|
||
- name: Find draft release & asset IDs (release PR)
|
||
if: contains(github.event.pull_request.labels.*.name, 'release')
|
||
id: fetch_assets
|
||
uses: actions/github-script@v7
|
||
with:
|
||
github-token: ${{ secrets.GH_PAT }}
|
||
script: |
|
||
const tag = '${{ steps.get_tag.outputs.tag }}';
|
||
const releases = await github.rest.repos.listReleases({
|
||
owner: context.repo.owner,
|
||
repo: context.repo.repo,
|
||
per_page: 100
|
||
});
|
||
const draft = releases.data.find(r => r.tag_name === tag && r.draft);
|
||
if (!draft) {
|
||
core.setFailed(`Draft release '${tag}' not found`);
|
||
return;
|
||
}
|
||
const find = (n) => draft.assets.find(a => a.name === n)?.id;
|
||
const diskId = find('nocloud-amd64.raw.xz');
|
||
if (!diskId) {
|
||
core.setFailed('Required assets missing in draft release');
|
||
return;
|
||
}
|
||
core.setOutput('disk_id', diskId);
|
||
|
||
|
||
e2e:
|
||
name: "E2E Tests"
|
||
runs-on: ${{ contains(github.event.pull_request.labels.*.name, 'debug') && 'self-hosted' || 'oracle-vm-24cpu-96gb-x86-64' }}
|
||
#runs-on: [oracle-vm-32cpu-128gb-x86-64]
|
||
timeout-minutes: 120
|
||
permissions:
|
||
contents: read
|
||
packages: read
|
||
needs: ["build", "resolve_assets"]
|
||
if: ${{ always() && (needs.build.result == 'success' || needs.resolve_assets.result == 'success') }}
|
||
|
||
steps:
|
||
# ▸ Checkout and prepare the codebase
|
||
- name: Checkout code
|
||
uses: actions/checkout@v4
|
||
|
||
# ▸ Regular PR path – download artefacts produced by the *build* job
|
||
- name: "Download Talos image (regular PR)"
|
||
if: "!contains(github.event.pull_request.labels.*.name, 'release')"
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: talos-image
|
||
path: _out/assets
|
||
|
||
- name: Download PR patch
|
||
if: "!contains(github.event.pull_request.labels.*.name, 'release')"
|
||
uses: actions/download-artifact@v4
|
||
with:
|
||
name: pr-patch
|
||
path: _out/assets
|
||
|
||
- name: Apply patch
|
||
if: "!contains(github.event.pull_request.labels.*.name, 'release')"
|
||
run: |
|
||
git apply _out/assets/pr.patch
|
||
|
||
# ▸ Release PR path – fetch artefacts from the corresponding draft release
|
||
- name: Download assets from draft release (release PR)
|
||
if: contains(github.event.pull_request.labels.*.name, 'release')
|
||
run: |
|
||
mkdir -p _out/assets
|
||
curl -sSL -H "Authorization: token ${GH_PAT}" -H "Accept: application/octet-stream" \
|
||
-o _out/assets/nocloud-amd64.raw.xz \
|
||
"https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/assets/${{ needs.resolve_assets.outputs.disk_id }}"
|
||
env:
|
||
GH_PAT: ${{ secrets.GH_PAT }}
|
||
|
||
- name: Set sandbox ID
|
||
run: echo "SANDBOX_NAME=cozy-e2e-sandbox-$(echo "${GITHUB_REPOSITORY}:${GITHUB_WORKFLOW}:${GITHUB_REF}" | sha256sum | cut -c1-10)" >> $GITHUB_ENV
|
||
|
||
# ▸ Prepare environment
|
||
- name: Prepare workspace
|
||
run: |
|
||
rm -rf /tmp/$SANDBOX_NAME
|
||
cp -r ${{ github.workspace }} /tmp/$SANDBOX_NAME
|
||
|
||
- name: Prepare environment
|
||
run: |
|
||
cd /tmp/$SANDBOX_NAME
|
||
attempt=0
|
||
until make SANDBOX_NAME=$SANDBOX_NAME prepare-env; do
|
||
attempt=$((attempt + 1))
|
||
if [ $attempt -ge 3 ]; then
|
||
echo "❌ Attempt $attempt failed, exiting..."
|
||
exit 1
|
||
fi
|
||
echo "❌ Attempt $attempt failed, retrying..."
|
||
done
|
||
echo "✅ The task completed successfully after $attempt attempts"
|
||
|
||
# ▸ Install Cozystack
|
||
- name: Install Cozystack into sandbox
|
||
run: |
|
||
cd /tmp/$SANDBOX_NAME
|
||
attempt=0
|
||
until make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME install-cozystack; do
|
||
attempt=$((attempt + 1))
|
||
if [ $attempt -ge 3 ]; then
|
||
echo "❌ Attempt $attempt failed, exiting..."
|
||
exit 1
|
||
fi
|
||
echo "❌ Attempt $attempt failed, retrying..."
|
||
done
|
||
echo "✅ The task completed successfully after $attempt attempts"
|
||
|
||
- name: Run OpenAPI tests
|
||
run: |
|
||
cd /tmp/$SANDBOX_NAME
|
||
make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME test-openapi
|
||
|
||
# ▸ Run E2E tests
|
||
- name: Run E2E tests
|
||
id: e2e_tests
|
||
run: |
|
||
cd /tmp/$SANDBOX_NAME
|
||
failed_tests=""
|
||
for app in $(ls hack/e2e-apps/*.bats | xargs -n1 basename | cut -d. -f1); do
|
||
echo "::group::Testing $app"
|
||
attempt=0
|
||
success=false
|
||
until [ $attempt -ge 3 ]; do
|
||
if make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME test-apps-$app; then
|
||
success=true
|
||
break
|
||
fi
|
||
attempt=$((attempt + 1))
|
||
echo "❌ Attempt $attempt failed, retrying..."
|
||
done
|
||
if [ "$success" = true ]; then
|
||
echo "✅ Test $app completed successfully"
|
||
else
|
||
echo "❌ Test $app failed after $attempt attempts"
|
||
failed_tests="$failed_tests $app"
|
||
fi
|
||
echo "::endgroup::"
|
||
done
|
||
if [ -n "$failed_tests" ]; then
|
||
echo "❌ Failed tests:$failed_tests"
|
||
exit 1
|
||
fi
|
||
echo "✅ All E2E tests passed"
|
||
|
||
# ▸ Collect debug information (always runs)
|
||
- name: Collect report
|
||
if: always()
|
||
run: |
|
||
cd /tmp/$SANDBOX_NAME
|
||
make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME collect-report || true
|
||
|
||
- name: Upload cozyreport.tgz
|
||
if: always()
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: cozyreport
|
||
path: /tmp/${{ env.SANDBOX_NAME }}/_out/cozyreport.tgz
|
||
|
||
- name: Collect images list
|
||
if: always()
|
||
run: |
|
||
cd /tmp/$SANDBOX_NAME
|
||
make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME collect-images || true
|
||
|
||
- name: Upload image list
|
||
if: always()
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: image-list
|
||
path: /tmp/${{ env.SANDBOX_NAME }}/_out/images.txt
|
||
|
||
# ▸ Tear down environment (always runs)
|
||
- name: Tear down sandbox
|
||
if: always()
|
||
run: make -C packages/core/testing SANDBOX_NAME=$SANDBOX_NAME delete || true
|
||
|
||
- name: Remove workspace
|
||
if: always()
|
||
run: rm -rf /tmp/$SANDBOX_NAME
|