From b610cee8b1c98ad8aaf6cf8867b5ef0f50c02771 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 19 Jun 2026 07:54:21 -0700 Subject: [PATCH 1/6] Update Workflow --- .github/workflows/xcodebuild.yml | 12 ++++++++++-- README.md | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/xcodebuild.yml b/.github/workflows/xcodebuild.yml index 6b9ceb0..665de76 100644 --- a/.github/workflows/xcodebuild.yml +++ b/.github/workflows/xcodebuild.yml @@ -72,6 +72,7 @@ on: test: description: | A flag indicating if the tests of the Xcode project scheme should run when using xcodebuild. + When codeql is enabled, the workflow builds without running tests regardless of this value. required: false type: boolean default: true @@ -182,6 +183,7 @@ jobs: - name: Build and test env: BUILD_CONFIG: ${{ inputs.buildConfig }} + CODEQL_ENABLED: ${{ inputs.codeql }} DESTINATION: ${{ inputs.destination }} RESULT_BUNDLE_INPUT: ${{ inputs.resultBundle }} SCHEME: ${{ inputs.scheme }} @@ -190,7 +192,13 @@ jobs: TEST_ENABLED: ${{ inputs.test }} TESTPLAN: ${{ inputs.testplan }} run: | - if [ "$TEST_ENABLED" = "true" ]; then + if [ "$CODEQL_ENABLED" = "true" ]; then + effective_test_enabled=false + else + effective_test_enabled="$TEST_ENABLED" + fi + + if [ "$effective_test_enabled" = "true" ]; then xcode_command="test" code_coverage_args=(-enableCodeCoverage YES) else @@ -218,7 +226,7 @@ jobs: other_swift_flags="$other_swift_flags -swift-version $SWIFT_VERSION" fi - if [ -n "$TESTPLAN" ]; then + if [ "$effective_test_enabled" = "true" ] && [ -n "$TESTPLAN" ]; then testplan_args=(-testPlan "$TESTPLAN") else testplan_args=() diff --git a/README.md b/README.md index c0a9bf3..4eae2a2 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,7 @@ jobs: CodeQL analysis uses the same workflow with `codeql: true`. Because GitHub sets unspecified token scopes to `none` when any explicit permission is declared, grant both `contents: read` and `security-events: write` on the calling job. +When `codeql` is enabled, the workflow builds the project without running tests even though `test` defaults to `true`. ```yml jobs: From 7b86a8e90e8af5c4e469d568e5b1dd61edf391e3 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 19 Jun 2026 14:13:59 -0700 Subject: [PATCH 2/6] Update --- .github/workflows/xcodebuild.yml | 4 ++-- README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/xcodebuild.yml b/.github/workflows/xcodebuild.yml index 665de76..730dff4 100644 --- a/.github/workflows/xcodebuild.yml +++ b/.github/workflows/xcodebuild.yml @@ -72,7 +72,7 @@ on: test: description: | A flag indicating if the tests of the Xcode project scheme should run when using xcodebuild. - When codeql is enabled, the workflow builds without running tests regardless of this value. + When CodeQL runs, the workflow builds without running tests regardless of this value. required: false type: boolean default: true @@ -183,7 +183,7 @@ jobs: - name: Build and test env: BUILD_CONFIG: ${{ inputs.buildConfig }} - CODEQL_ENABLED: ${{ inputs.codeql }} + CODEQL_ENABLED: ${{ runner.environment == 'github-hosted' && inputs.codeql }} DESTINATION: ${{ inputs.destination }} RESULT_BUNDLE_INPUT: ${{ inputs.resultBundle }} SCHEME: ${{ inputs.scheme }} diff --git a/README.md b/README.md index 4eae2a2..9b92e36 100644 --- a/README.md +++ b/README.md @@ -323,7 +323,7 @@ jobs: CodeQL analysis uses the same workflow with `codeql: true`. Because GitHub sets unspecified token scopes to `none` when any explicit permission is declared, grant both `contents: read` and `security-events: write` on the calling job. -When `codeql` is enabled, the workflow builds the project without running tests even though `test` defaults to `true`. +When CodeQL runs on a GitHub-hosted runner, the workflow builds the project without running tests even though `test` defaults to `true`. ```yml jobs: From b086930a88d51faba5742e4f7bc883ac48b950de Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 19 Jun 2026 14:17:01 -0700 Subject: [PATCH 3/6] Update MD check --- .github/workflows/markdown-links.yml | 2 +- README.md | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/markdown-links.yml b/.github/workflows/markdown-links.yml index 985dffe..829eaf9 100644 --- a/.github/workflows/markdown-links.yml +++ b/.github/workflows/markdown-links.yml @@ -16,7 +16,7 @@ on: JSON-based collection of labels indicating which type of GitHub runner should be chosen. required: false type: string - default: '["macOS", "self-hosted"]' + default: '["ubuntu-latest"]' permissions: contents: read diff --git a/README.md b/README.md index 9b92e36..cdd6145 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,6 @@ jobs: markdown-links: name: Check Markdown Links uses: SchmiedmayerLab/.github/.github/workflows/markdown-links.yml@v0.2 - with: - runs_on_labels: '["ubuntu-latest"]' ``` ##### Run Periphery From 7014ed77b0def7b517d9abfff32f24d155e2f550 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 19 Jun 2026 14:17:16 -0700 Subject: [PATCH 4/6] Update --- .github/workflows/validate.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 449e7e2..812d8e3 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -28,8 +28,6 @@ jobs: markdown_link_check: name: Check Markdown Links uses: ./.github/workflows/markdown-links.yml - with: - runs_on_labels: '["ubuntu-latest"]' yamllint: name: Lint YAML runs-on: ubuntu-latest From 5f7d425a35ff4e1e83fd44bfee0103654baefd72 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 19 Jun 2026 14:19:06 -0700 Subject: [PATCH 5/6] Add Concurrency --- .github/workflows/release.yml | 4 ++++ .github/workflows/validate.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 89aa537..00acaab 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,10 @@ on: types: [created] workflow_dispatch: +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + jobs: releasetag: name: Tag Action Release diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 812d8e3..94ef1ed 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -18,6 +18,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: reuse_action: name: Check REUSE Compliance From 13e39f3e68744d7d80adafc2978e0b9bc2596668 Mon Sep 17 00:00:00 2001 From: Paul Schmiedmayer Date: Fri, 19 Jun 2026 14:42:58 -0700 Subject: [PATCH 6/6] Improve Workflows --- .github/workflows/firebase-emulators-exec.yml | 28 ++++ .github/workflows/swift-package-test.yml | 2 - .github/workflows/xcodebuild.yml | 131 +++++++++++++++--- README.md | 18 ++- 4 files changed, 157 insertions(+), 22 deletions(-) diff --git a/.github/workflows/firebase-emulators-exec.yml b/.github/workflows/firebase-emulators-exec.yml index bce8ce9..dd474d7 100644 --- a/.github/workflows/firebase-emulators-exec.yml +++ b/.github/workflows/firebase-emulators-exec.yml @@ -67,6 +67,13 @@ on: required: false type: boolean default: false + artifact_path: + description: | + File or directory path, relative to path, to upload after the command runs. + The artifact name is inferred from the final path component. + required: false + type: string + default: '' secrets: GOOGLE_APPLICATION_CREDENTIALS_BASE64: description: | @@ -136,6 +143,27 @@ jobs: else firebase emulators:exec -c "$FIREBASE_JSON_PATH" "$COMMAND" fi + - name: Resolve artifact name + id: artifact + if: ${{ (success() || failure()) && inputs.artifact_path != '' }} + env: + ARTIFACT_PATH: ${{ inputs.artifact_path }} + run: | + normalized_path="${ARTIFACT_PATH%/}" + artifact_name="$(basename "$normalized_path")" + + if [ -z "$artifact_name" ] || [ "$artifact_name" = "." ]; then + artifact_name="artifact" + fi + + artifact_name="$(printf '%s' "$artifact_name" | tr '/\\:*?"<>|' '-')" + echo "name=$artifact_name" >> "$GITHUB_OUTPUT" + - name: Upload artifact + if: ${{ (success() || failure()) && inputs.artifact_path != '' }} + uses: actions/upload-artifact@v7 + with: + name: ${{ steps.artifact.outputs.name }} + path: ${{ format('{0}/{1}', inputs.path, inputs.artifact_path) }} - name: Clean up Google application credentials if: always() run: | diff --git a/.github/workflows/swift-package-test.yml b/.github/workflows/swift-package-test.yml index 58968eb..88defa2 100644 --- a/.github/workflows/swift-package-test.yml +++ b/.github/workflows/swift-package-test.yml @@ -67,7 +67,6 @@ jobs: destination: ${{ matrix.platform.destination }} buildConfig: ${{ matrix.config }} resultBundle: ${{ format('{0}-{1}-{2}.xcresult', inputs.package_name, matrix.platform.name, matrix.config) }} - artifactname: ${{ format('{0}-{1}-{2}.xcresult', inputs.package_name, matrix.platform.name, matrix.config) }} ui_tests: name: UI ${{ matrix.platform.name }} (${{ matrix.config }}) @@ -86,7 +85,6 @@ jobs: destination: ${{ matrix.platform.destination }} buildConfig: ${{ matrix.config }} resultBundle: ${{ format('{0}-{1}-{2}.xcresult', matrix.platform.scheme, matrix.platform.name, matrix.config) }} - artifactname: ${{ format('{0}-{1}-{2}.xcresult', matrix.platform.scheme, matrix.platform.name, matrix.config) }} package_tests_linux: name: Linux (${{ matrix.config }}) diff --git a/.github/workflows/xcodebuild.yml b/.github/workflows/xcodebuild.yml index 730dff4..1ae0b57 100644 --- a/.github/workflows/xcodebuild.yml +++ b/.github/workflows/xcodebuild.yml @@ -39,8 +39,10 @@ on: scheme: description: | The scheme in the Xcode project. - required: true + If omitted, the workflow infers the Swift package scheme from Package.swift or the only shared Xcode scheme. + required: false type: string + default: '' buildConfig: description: | The build configuration parameter that should be passed to xcodebuild. @@ -59,7 +61,7 @@ on: resultBundle: description: | The name of the Xcode result bundle that is passed to xcodebuild. - If not defined, the name of the scheme + .xcresult is used. + If not defined, Swift packages use the package name + .xcresult and Xcode projects use the resolved scheme + .xcresult. required: false type: string default: '' @@ -90,6 +92,7 @@ on: artifactname: description: | The name and path of the artifact that should be uploaded at the end of the build. + If not defined, test runs upload the resolved result bundle using its file name as the artifact name. required: false type: string default: '' @@ -158,14 +161,116 @@ jobs: cp "$XCODE_PATH"/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/host/*.dylib "$XCODE_PATH/Toolchains/XcodeDefault.xctoolchain/usr/lib" sudo mkdir -p /usr/local/lib sudo cp "$XCODE_PATH"/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/host/*.dylib /usr/local/lib + - name: Resolve xcodebuild inputs + id: xcodebuild_inputs + env: + ARTIFACT_INPUT: ${{ inputs.artifactname }} + CODEQL_ENABLED: ${{ runner.environment == 'github-hosted' && inputs.codeql }} + RESULT_BUNDLE_INPUT: ${{ inputs.resultBundle }} + SCHEME_INPUT: ${{ inputs.scheme }} + TEST_ENABLED: ${{ inputs.test }} + run: | + package_name="" + + if [ -f Package.swift ]; then + package_dump="$(swift package dump-package)" + package_name="$(echo "$package_dump" | jq -r '.name')" + fi + + if [ -n "$SCHEME_INPUT" ]; then + scheme="$SCHEME_INPUT" + elif [ -n "$package_name" ]; then + library_products="$(echo "$package_dump" | jq -c '[.products[] | select(.type | tostring | test("library")) | .name]')" + library_product_count="$(echo "$library_products" | jq 'length')" + if [ "$library_product_count" -gt 1 ]; then + scheme="${package_name}-Package" + else + scheme="$package_name" + fi + else + workspaces=() + while IFS= read -r workspace; do + workspaces+=("$workspace") + done < <(find . -maxdepth 1 -name "*.xcworkspace" -type d | sort) + + projects=() + while IFS= read -r project; do + projects+=("$project") + done < <(find . -maxdepth 1 -name "*.xcodeproj" -type d | sort) + + if [ "${#workspaces[@]}" -eq 1 ]; then + container_args=(-workspace "${workspaces[0]#./}") + elif [ "${#workspaces[@]}" -gt 1 ]; then + echo "::error::Multiple Xcode workspaces found. Set the scheme input explicitly." + exit 1 + elif [ "${#projects[@]}" -eq 1 ]; then + container_args=(-project "${projects[0]#./}") + elif [ "${#projects[@]}" -gt 1 ]; then + echo "::error::Multiple Xcode projects found. Set the scheme input explicitly." + exit 1 + else + echo "::error::No Package.swift, .xcworkspace, or .xcodeproj found. Set path to the project directory." + exit 1 + fi + + schemes="$(xcodebuild -list -json "${container_args[@]}" | ruby -rjson -e ' + data = JSON.parse(STDIN.read) + puts(data.dig("workspace", "schemes") || data.dig("project", "schemes") || []) + ')" + scheme_count="$(printf '%s\n' "$schemes" | sed '/^$/d' | wc -l | tr -d ' ')" + if [ "$scheme_count" -ne 1 ]; then + echo "::error::Expected exactly one shared Xcode scheme, found $scheme_count. Set the scheme input explicitly." + printf '%s\n' "$schemes" + exit 1 + fi + + scheme="$schemes" + fi + + if [ -z "$RESULT_BUNDLE_INPUT" ]; then + if [ -n "$package_name" ]; then + result_bundle="${package_name}.xcresult" + else + result_bundle="${scheme}.xcresult" + fi + else + result_bundle="$RESULT_BUNDLE_INPUT" + fi + + if [ -n "$ARTIFACT_INPUT" ]; then + artifact_path="$ARTIFACT_INPUT" + elif [ "$CODEQL_ENABLED" != "true" ] && [ "$TEST_ENABLED" = "true" ]; then + artifact_path="$result_bundle" + else + artifact_path="" + fi + + if [ -n "$artifact_path" ]; then + normalized_artifact_path="${artifact_path%/}" + artifact_name="$(basename "$normalized_artifact_path")" + if [ -z "$artifact_name" ] || [ "$artifact_name" = "." ]; then + artifact_name="artifact" + fi + + artifact_name="$(printf '%s' "$artifact_name" | tr '/\\:*?"<>|' '-')" + else + artifact_name="" + fi + + { + echo "scheme=$scheme" + echo "result_bundle=$result_bundle" + echo "artifact_path=$artifact_path" + echo "artifact_name=$artifact_name" + } >> "$GITHUB_OUTPUT" - name: Check available simulators env: - SCHEME: ${{ inputs.scheme }} + SCHEME: ${{ steps.xcodebuild_inputs.outputs.scheme }} run: | xcrun xcodebuild -scheme "$SCHEME" -showdestinations - name: Resolve dependencies env: - SCHEME: ${{ inputs.scheme }} + SCHEME: ${{ steps.xcodebuild_inputs.outputs.scheme }} SPM_DISABLE_PREBUILTS: ${{ inputs.spm-disable-prebuilts }} run: | prebuilt_args=() @@ -185,8 +290,8 @@ jobs: BUILD_CONFIG: ${{ inputs.buildConfig }} CODEQL_ENABLED: ${{ runner.environment == 'github-hosted' && inputs.codeql }} DESTINATION: ${{ inputs.destination }} - RESULT_BUNDLE_INPUT: ${{ inputs.resultBundle }} - SCHEME: ${{ inputs.scheme }} + RESULT_BUNDLE: ${{ steps.xcodebuild_inputs.outputs.result_bundle }} + SCHEME: ${{ steps.xcodebuild_inputs.outputs.scheme }} SPM_DISABLE_PREBUILTS: ${{ inputs.spm-disable-prebuilts }} SWIFT_VERSION: ${{ inputs.swiftVersion }} TEST_ENABLED: ${{ inputs.test }} @@ -206,12 +311,6 @@ jobs: code_coverage_args=() fi - if [ -z "$RESULT_BUNDLE_INPUT" ]; then - result_bundle="$SCHEME.xcresult" - else - result_bundle="$RESULT_BUNDLE_INPUT" - fi - if [ "$BUILD_CONFIG" = "Release" ]; then enable_testing_flag="-enable-testing" else @@ -246,7 +345,7 @@ jobs: "${testplan_args[@]}" \ "${code_coverage_args[@]}" \ -derivedDataPath ".derivedData" \ - -resultBundlePath "$result_bundle" \ + -resultBundlePath "$RESULT_BUNDLE" \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGN_IDENTITY="" \ OTHER_SWIFT_FLAGS="$other_swift_flags" \ @@ -258,8 +357,8 @@ jobs: if: ${{ runner.environment == 'github-hosted' && inputs.codeql }} uses: github/codeql-action/analyze@v4 - name: Upload artifact - if: ${{ (success() || failure()) && inputs.artifactname != '' }} + if: ${{ (success() || failure()) && steps.xcodebuild_inputs.outputs.artifact_path != '' }} uses: actions/upload-artifact@v7 with: - name: ${{ inputs.artifactname }} - path: ${{ inputs.path }}/${{ inputs.artifactname }} + name: ${{ steps.xcodebuild_inputs.outputs.artifact_name }} + path: ${{ inputs.path }}/${{ steps.xcodebuild_inputs.outputs.artifact_path }} diff --git a/README.md b/README.md index cdd6145..cd9ca05 100644 --- a/README.md +++ b/README.md @@ -303,20 +303,28 @@ jobs: ##### Build and Test with xcodebuild Use [`xcodebuild.yml`](.github/workflows/xcodebuild.yml) for Apple projects that need direct xcodebuild tests or builds. +When `scheme` is omitted, the workflow infers the Swift package scheme from `Package.swift` or the only shared Xcode scheme in the selected `path`. +Swift packages use the package name as the scheme, or `PackageName-Package` when the package defines multiple library products. +Swift package result bundles use `PackageName.xcresult` in both cases. +Test runs upload the resolved `.xcresult` bundle automatically; set `resultBundle` only when you need a custom bundle name. The workflow intentionally does not declare its own `permissions` block because its CodeQL path is optional. Callers that set `codeql: true` must grant `security-events: write`; normal build and test jobs can omit that permission. ```yml jobs: - build-and-test: - name: Build and Test Swift Package + app-tests: + name: Build and Test App permissions: contents: read uses: SchmiedmayerLab/.github/.github/workflows/xcodebuild.yml@v0.2 with: - artifactname: TemplatePackage.xcresult runsonlabels: '["macOS", "self-hosted"]' - scheme: TemplatePackage + package-tests: + name: Build and Test Swift Package + uses: SchmiedmayerLab/.github/.github/workflows/xcodebuild.yml@v0.2 + with: + path: ExamplePackage + runsonlabels: '["macOS", "self-hosted"]' ``` CodeQL analysis uses the same workflow with `codeql: true`. @@ -340,6 +348,7 @@ jobs: Use [`firebase-emulators-exec.yml`](.github/workflows/firebase-emulators-exec.yml) for test or validation commands that must run while Firebase emulators are active. The `command` input is executed through `firebase emulators:exec` and can be any trusted shell command. +Use `artifact_path` to upload command output such as an `.xcresult` bundle; the artifact name is inferred from the path. ```yml jobs: @@ -348,6 +357,7 @@ jobs: uses: SchmiedmayerLab/.github/.github/workflows/firebase-emulators-exec.yml@v0.2 with: command: bundle exec fastlane uitest + artifact_path: fastlane/test_output/UITests.xcresult firebase_emulator_import: ./firebase-export secrets: GOOGLE_APPLICATION_CREDENTIALS_BASE64: ${{ secrets.GOOGLE_APPLICATION_CREDENTIALS_BASE64 }}