Skip to content
Open
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
87 changes: 87 additions & 0 deletions .github/actions/run-store/get-junit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Discover exact JUnit keys from authenticated producer step names, not wildcards."""
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile


def api(path):
env = dict(os.environ, GH_TOKEN=os.environ.get("ACTIONS_TOKEN") or os.environ.get("GH_TOKEN", ""))
return json.loads(subprocess.check_output(["gh", "api", path], env=env, text=True, timeout=60))


def entries(rows, run_id):
names = set()
for job in rows:
if str(job.get("run_id")) != str(run_id):
raise ValueError("job belongs to another run")
for step in job.get("steps", []):
name = step.get("name", "")
if step.get("conclusion") == "skipped":
continue
if name.startswith("run-store put junit-"):
name = name[len("run-store put "):]
if not re.fullmatch(r"junit-[A-Za-z0-9._-]{1,94}", name):
raise ValueError("invalid JUnit discovery name")
names.add(name)
return sorted(names)


def fetch(repo, run_id, destination):
if not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repo) or not re.fullmatch(r"[0-9]{1,20}", str(run_id)):
raise ValueError("invalid repository or run id")
repository_id = str(api(f"repos/{repo}")["id"])
rows = []
page = 1
while True:
batch = api(f"repos/{repo}/actions/runs/{run_id}/jobs?filter=latest&per_page=100&page={page}")["jobs"]
rows.extend(batch)
if len(batch) < 100:
break
page += 1
names = entries(rows, run_id)
lanes = {}
for job in rows:
for name in entries([job], run_id):
lane = job.get("name")
if not isinstance(lane, str) or not lane.strip():
raise ValueError("producer job has no lane name")
lane = lane.replace(",", ";")
if name in lanes and lanes[name] != lane:
raise ValueError("JUnit key is shared by different jobs")
lanes[name] = lane
found = bool(names)
with tempfile.TemporaryDirectory(prefix="junit-store-") as tmp:
stage = Path(tmp) / "entries"
for index, name in enumerate(names):
output = Path(tmp) / f"output-{index}"
env = dict(os.environ, MODE="get", NAME=name, STORE_PATH=str(stage / name),
STORE_RUN_ID=str(run_id), REPO_ID=repository_id, REQUIRED="false", GITHUB_OUTPUT=str(output))
subprocess.run(["bash", str(Path(__file__).with_name("run-store.sh"))], env=env, check=True, timeout=3600)
if not output.exists() or "found=true" not in output.read_text().splitlines():
found = False
break
# Never expose a partial shard set as evidence that a failed test passed.
if found:
Path(destination).mkdir(parents=True, exist_ok=True)
for name in names:
shutil.move(str(stage / name), str(Path(destination) / name))
(Path(destination) / ".run-store-lanes.json").write_text(json.dumps(lanes))
if os.environ.get("GITHUB_OUTPUT"):
with open(os.environ["GITHUB_OUTPUT"], "a") as output:
output.write(f"found={str(found).lower()}\n")
return found


if __name__ == "__main__":
try:
repo, run_id, destination = sys.argv[1:]
sys.exit(0 if fetch(repo, run_id, destination) else 1)
except Exception as error:
print(f"::warning::JUnit run-store discovery failed: {error}", file=sys.stderr)
sys.exit(1)
38 changes: 18 additions & 20 deletions .github/workflows/ios-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ on:
inputs:
ipa-artifact:
description: >
Prebuilt mode. Name of an artifact holding exactly one signed .ipa,
uploaded by an earlier job of the caller's same run (which this job
Prebuilt mode. Name of a run-store entry holding exactly one signed .ipa,
stored by an earlier job of the caller's same run (which this job
`needs:`). When set, checkout, keychain, archive and export are skipped.
required: false
type: string
default: ''
xcode-artifact:
description: >
Build mode only. Name of an artifact holding an exported Xcode
project (for example Unity's export from Linux), uploaded by an
earlier job of the caller's same run. Downloaded into the checkout
Build mode only. Name of a run-store entry holding an exported Xcode
project (for example Unity's export from Linux), stored by an
earlier job of the caller's same run. Fetched into the checkout
at artifact-path before the build.
required: false
type: string
Expand Down Expand Up @@ -125,6 +125,7 @@ on:

permissions:
contents: read
id-token: write

jobs:
prepare:
Expand Down Expand Up @@ -207,11 +208,12 @@ jobs:
echo "::error::Xcode $IN_XCODE_VERSION is not installed; installed:${installed:- none}"
exit 1

- name: Download Xcode project artifact
- name: Download Xcode project entry
if: inputs.ipa-artifact == '' && inputs.xcode-artifact != ''
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: SylphxAI/.github/.github/actions/run-store@c46a1cd933a242b1471afb26850ff5d8f5d52800
timeout-minutes: 10
with:
mode: get
name: ${{ inputs.xcode-artifact }}
path: ${{ inputs.artifact-path }}

Expand Down Expand Up @@ -240,15 +242,12 @@ jobs:

- name: Upload prepared workspace
if: inputs.ipa-artifact == ''
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
# Quota exhaustion must never eject a merge_group entry; elsewhere a failed upload stays loud.
continue-on-error: ${{ github.event_name == 'merge_group' }}
uses: SylphxAI/.github/.github/actions/run-store@c46a1cd933a242b1471afb26850ff5d8f5d52800
timeout-minutes: 15
with:
mode: put
name: ios-workspace-${{ inputs.bundle-id }}
path: ${{ runner.temp }}/ios-workspace.tar
retention-days: 1
if-no-files-found: error


sign:
Expand Down Expand Up @@ -438,9 +437,10 @@ jobs:
exit 1

- name: Download prepared workspace
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: SylphxAI/.github/.github/actions/run-store@c46a1cd933a242b1471afb26850ff5d8f5d52800
timeout-minutes: 15
with:
mode: get
name: ios-workspace-${{ inputs.bundle-id }}
path: ${{ runner.temp }}/ios-signing/workspace-dl

Expand Down Expand Up @@ -606,14 +606,11 @@ jobs:

- name: Upload exported .ipa
timeout-minutes: 15
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
# Quota exhaustion must never eject a merge_group entry; elsewhere a failed upload stays loud.
continue-on-error: ${{ github.event_name == 'merge_group' }}
uses: SylphxAI/.github/.github/actions/run-store@c46a1cd933a242b1471afb26850ff5d8f5d52800
with:
mode: put
name: ios-ipa-${{ inputs.bundle-id }}
path: ${{ runner.temp }}/ios-signing/export/*.ipa
retention-days: 1
if-no-files-found: error
path: ${{ runner.temp }}/ios-signing/export

- name: Cleanup (keychain, profiles, API key, temporary files)
if: always()
Expand Down Expand Up @@ -806,9 +803,10 @@ jobs:
done

- name: Download .ipa
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: SylphxAI/.github/.github/actions/run-store@c46a1cd933a242b1471afb26850ff5d8f5d52800
timeout-minutes: 10
with:
mode: get
name: ${{ inputs.ipa-artifact != '' && inputs.ipa-artifact || format('ios-ipa-{0}', inputs.bundle-id) }}
path: ${{ runner.temp }}/ios-signing/export

Expand Down
19 changes: 19 additions & 0 deletions .github/workflows/junit-consumer-control.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Cross-run JUnit consumer control

on:
workflow_run:
workflows: [Project control]
types: [completed]

permissions:
actions: read
contents: read
id-token: write

jobs:
consumer:
# Dispatch already calls the same consumer at the selected PR ref.
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event != 'workflow_dispatch'
uses: ./.github/workflows/junit-consumer.yml
with:
producer-run: ${{ format('{0}', github.event.workflow_run.id) }}
36 changes: 36 additions & 0 deletions .github/workflows/junit-consumer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: JUnit run-store consumer

on:
workflow_call:
inputs:
producer-run:
description: Completed Project control run containing the two JUnit fixture shards.
required: true
type: string

permissions:
actions: read
contents: read
id-token: write

jobs:
consume:
runs-on: ${{ github.event.repository.private && 'sylphx-linux-standard' || 'ubuntu-latest' }}
timeout-minutes: 10
steps:
# Same source binding as red-main: never execute a producer commit.
- name: Read canonical run-store consumer
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
repository: SylphxAI/.github
ref: ${{ job.workflow_sha }}
persist-credentials: false
path: consumer-shared
- name: Install parser dependency
run: python3 -m pip install --quiet pyyaml
- name: Fetch exact cross-run JUnit and verify identity
env:
GH_TOKEN: ${{ github.token }}
PRODUCER_RUN: ${{ inputs.producer-run }}
CONSUMER_SOURCE: ${{ job.workflow_sha }}
run: python3 consumer-shared/scripts/check-junit-consumer.py
54 changes: 53 additions & 1 deletion .github/workflows/project-control.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ on:
branches:
- main
pull_request:
workflow_dispatch:
inputs:
junit-producer-run:
description: 'Completed Project control producer run to read from a different run.'
required: true
type: string

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref_name }}
Expand Down Expand Up @@ -51,6 +57,13 @@ jobs:
run: python -m pip install --quiet pyyaml
- name: Unit tests
run: python -m unittest discover -s tests
- name: Lint authenticated JUnit consumer workflows
uses: ./.github/actions/workflow-lint
with:
args: |
.github/workflows/project-control.yml
.github/workflows/junit-consumer.yml
.github/workflows/junit-consumer-control.yml
- name: Prepare metadata sync consumer fixture
env:
SYNC_FIXTURE: ${{ runner.temp }}/metadata-consumer
Expand Down Expand Up @@ -100,6 +113,45 @@ jobs:
with:
brand-directory: ${{ runner.temp }}/brand-consumer/brand data

junit-producer:
if: github.event_name != 'workflow_dispatch'
name: JUnit fixture (${{ matrix.shard }})
# Standard hosted runners are free only while this repository is public.
runs-on: ${{ github.event.repository.private && 'sylphx-linux-standard' || 'ubuntu-latest' }}
timeout-minutes: 5
permissions:
contents: read
id-token: write
strategy:
matrix:
shard: [1, 701]
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Prepare nested passing and failing JUnit cases
env:
SHARD: ${{ matrix.shard }}
run: |
mkdir -p "$RUNNER_TEMP/junit/nested"
printf '<testsuite><testcase classname="suite" name="broken-%s"><failure/></testcase><testcase classname="suite" name="passing-%s"/></testsuite>\n' "$SHARD" "$SHARD" > "$RUNNER_TEMP/junit/nested/junit.xml"
- name: run-store put junit-consumer-${{ matrix.shard }}
uses: ./.github/actions/run-store
with:
mode: put
name: junit-consumer-${{ matrix.shard }}
path: ${{ runner.temp }}/junit

junit-consumer:
if: github.event_name == 'workflow_dispatch'
permissions:
actions: read
contents: read
id-token: write
uses: ./.github/workflows/junit-consumer.yml
with:
producer-run: ${{ inputs.junit-producer-run }}

# The optimistic-merge actions, run as a consumer runs them: a manifest the
# runner cannot load (an expression in a description, a bad input) fails
# here, before any repository pins it (#111). The starters are linted as
Expand Down Expand Up @@ -179,7 +231,7 @@ jobs:
# Aggregate verdict required by the main merge queue ruleset. Skipped jobs
# pass; failed or cancelled jobs fail.
ci-ok:
needs: [project-control, tests, template-plan, template-verdict]
needs: [project-control, tests, template-plan, template-verdict, junit-producer, junit-consumer]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
Expand Down
Loading
Loading