Skip to content

test: use the quinaryLabel spelling CI's Xcode accepts (#3397) #2075

test: use the quinaryLabel spelling CI's Xcode accepts (#3397)

test: use the quinaryLabel spelling CI's Xcode accepts (#3397) #2075

Workflow file for this run

name: Repo Hygiene
# Lints the parts of the repo the macOS suite cannot reach. 7,500 lines of shell build, sign,
# notarize and publish this app, and nothing checked any of it: the six findings actionlint reported
# the first time it ran here were all inside `run:` blocks in the release and plugin workflows.
#
# Runs on ubuntu, where minutes are free on a public repo, and finishes in well under a minute.
on:
# No paths filter on pull_request: a workflow skipped by one never reports, so its gate could not
# be a required check. The changes job below does the filtering instead.
pull_request:
paths:
- "scripts/**"
- ".claude/skills/**/*.sh"
- ".github/workflows/**"
- ".github/actions/**"
- ".github/scripts/**"
- ".github/plugin-registry.json"
- ".github/duplicate-contract-baseline.txt"
- "Plugins/**/*.swift"
- "TablePro/**/*.swift"
- "Packages/**/*.swift"
- "TableProTests/**/*.swift"
- "project.yml"
- "TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
- "TablePro/Resources/ThirdPartyLicenses/licenses.yml"
- ".swiftlint.yml"
push:
branches: [main]
paths:
- "scripts/**"
- ".claude/skills/**/*.sh"
- ".github/workflows/**"
- ".github/actions/**"
- ".github/scripts/**"
- ".github/plugin-registry.json"
- ".github/duplicate-contract-baseline.txt"
- ".github/hub-ratchet-baseline.json"
- ".claude/skills/**/*.sh"
- "Plugins/**/*.swift"
- "TablePro/**/*.swift"
- "Packages/**/*.swift"
- "TableProTests/**/*.swift"
- "project.yml"
- "TablePro.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved"
- "TablePro/Resources/ThirdPartyLicenses/licenses.yml"
- ".swiftlint.yml"
workflow_dispatch:
concurrency:
group: repo-hygiene-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
# Keep this list and the push paths above identical.
changes:
name: Detect relevant changes
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
run: ${{ steps.decide.outputs.run }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- name: Decide whether the checks need to run
id: decide
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
RUN=$(scripts/ci/detect-changed-paths.sh \
--event "$EVENT_NAME" \
--base "$BASE_SHA" \
'scripts/' '\.github/workflows/' '\.github/actions/' '\.github/scripts/' \
'\.github/plugin-registry\.json$' '\.github/duplicate-contract-baseline\.txt$' \
'\.github/hub-ratchet-baseline\.json$' '\.claude/skills/.*\.sh$' \
'Plugins/.*\.swift$' 'TablePro/.*\.swift$' 'Packages/.*\.swift$' 'TableProTests/.*\.swift$' \
'project\.yml$' \
'TablePro\.xcodeproj/project\.xcworkspace/xcshareddata/swiftpm/Package\.resolved$' \
'TablePro/Resources/ThirdPartyLicenses/licenses\.yml$' '\.swiftlint\.yml$')
echo "run=$RUN" >> "$GITHUB_OUTPUT"
lint:
name: Lint workflows and scripts
needs: changes
if: needs.changes.outputs.run == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install shellcheck and actionlint
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq shellcheck
bash <(curl -sSL https://raw.githubusercontent.com/rhysd/actionlint/v1.7.12/scripts/download-actionlint.bash) 1.7.12
./actionlint --version
# actionlint shells out to shellcheck for every inline `run:` block, which is the only way
# those get checked at all: they are not files, so a plain shellcheck run never sees them.
- name: Lint workflows
run: ./actionlint -color
# --severity=warning is the floor the repo currently holds at zero. The remaining
# informational findings are almost all SC2012 (`ls` where `find` would be sturdier) in
# message strings, and gating on them today would only add noise.
- name: Lint scripts
run: |
set -euo pipefail
mapfile -t SCRIPTS < <(find scripts .claude/skills -name '*.sh' | sort)
echo "checking ${#SCRIPTS[@]} scripts"
shellcheck --severity=warning "${SCRIPTS[@]}"
# shellcheck cannot tell a shell function from a command on PATH, so a script that calls a
# scripts/lib function without sourcing the library reads as correct everywhere and dies at
# run time with exit 127. That is how v0.67.1's DMG job failed: create-dmg.sh called
# notarize_and_staple and never sourced lib/notarize.sh, and a release was the first thing
# to run it.
- name: Check every script sources the libraries it calls into
run: python3 scripts/ci/check-lib-sourcing.py
- name: Check the plugin manifest against the plugin classes
run: python3 scripts/ci/check-plugin-manifest.py
# A build script that writes the deployment target down itself goes stale the next time the
# app's floor moves, and the result is an archive built for a newer macOS than the app that
# links it. That shipped: #2874 moved the app to macOS 13 and five copies of "14.0" stayed.
- name: Check no build script hardcodes the deployment target
run: bash scripts/ci/check-deployment-target.sh
# The iOS app compiles source files that belong to macOS plugin targets, and it alone sets
# SWIFT_DEFAULT_ACTOR_ISOLATION: MainActor, so one unmarked declaration in a shared file is
# @MainActor on iOS and nonisolated in the plugin. The macOS suite cannot see it and the iOS
# job reports it one symbol at a time, which took main red three times over one keyword.
- name: Check shared plugin sources are explicitly isolated for iOS
run: python3 scripts/ci/check-ios-shared-isolation.py
# The plugin targets build with MemberImportVisibility, so a file that logs needs the import
# that defines the log methods. Compiling every plugin catches it, but only once both sides
# are merged: the pull request that turned the feature on and the pull request that added the
# file were each green alone, and main broke on the merge of the second.
- name: Check plugin sources that log import the module that defines it
run: python3 scripts/ci/check-plugin-os-import.py
# SwiftLint's included: does not reach Plugins/, so this applies its log privacy rule there,
# with the regex read from .swiftlint.yml.
- name: Check no source publishes an error's text to the system log
run: python3 scripts/ci/check-log-privacy.py
- name: Validate the log privacy check
run: python3 scripts/ci/test_check_log_privacy.py
# Every top-level @Suite is a peer macro the compiler files in module-scope lookup, and each
# @Suite expansion walks all of them, so the test module's compile time grows with the square
# of their count. 2,336 of them held TableProTests' emit-module job at 544 to 626 seconds on the
# build runner, and 2,200 of those only renamed their type. Only a @Suite with a trait is kept.
- name: Check no top-level test type carries a @Suite without a trait
run: python3 scripts/ci/check-test-suite-attributes.py
- name: Validate the test suite attribute check
run: python3 scripts/ci/test_check_test_suite_attributes.py
# A plugin's or package's String(localized:) resolves against Bundle.main, which is the host
# app, so its strings live in the app's catalog. Xcode never finds them in the app's sources,
# so each one must be there and managed manually, or Xcode's next sync deletes it.
- name: Check plugin strings reach the string catalog
run: python3 scripts/localization.py plugins
- name: Validate the localization script
run: python3 scripts/test_localization.py
- name: Validate the registry update script
run: python3 .github/scripts/test_update_registry.py
# Only the kit version a release tags ever ships, and the registry keeps binaries for three,
# so a second bump in one release cycle drops an older release out of that window for nothing.
# The checkout is shallow and carries no tags, so the check lists them on origin itself.
- name: Check currentPluginKitVersion moved at most once since the last release
run: python3 scripts/ci/check-pluginkit-bump-cadence.py --remote origin
- name: Validate the PluginKit bump cadence check
run: python3 scripts/ci/test_check_pluginkit_bump_cadence.py
# The macOS suite cannot catch a fault here: the lister runs before any test does.
- name: Validate the test lister and quarantine
run: python3 scripts/ci/test_list_tests.py
- name: Validate the pull request title check
run: python3 scripts/ci/test_check_pr_title.py
- name: Validate release note extraction
run: python3 scripts/ci/test_release_notes.py
- name: Validate the DMG script's arguments
run: bash scripts/ci/test_create_dmg_args.sh
- name: Validate the release draft script
run: bash scripts/ci/test_draft_release.sh
- name: Validate changelog contributor credits
run: python3 scripts/ci/test_changelog_credits.py
# Only a real release runs sign-and-appcast.sh, so nothing on a pull request ever exercised
# the code that edits the file every install polls. These two cover the parts that can be
# tested without a signing key: which bytes move, and which releases are refused.
- name: Validate the appcast text primitives
run: python3 scripts/ci/test_appcast_feed.py
- name: Validate the appcast merge
run: python3 scripts/ci/test_merge_appcast.py
- name: Validate the delta naming
run: python3 scripts/ci/test_name_deltas.py
- name: Validate release withdrawal
run: python3 scripts/ci/test_pull_release.py
# The framework the app links and the generate_appcast that signs its feed are the same
# project, and nothing compared them. project.yml declares majorVersion, which XcodeGen
# writes as upToNextMajorVersion, so a resolve can move the framework alone.
- name: Check the Sparkle version agrees everywhere
run: python3 scripts/ci/check-sparkle-version.py
# A check that guarded a real invariant and ran nowhere. Pure grep over Swift sources, so it
# belongs on the free Linux runner. The MongoDB filter-shape check is the other one that was
# orphaned, but it compiles a C probe against Libs/libbson, so it lives in the macOS build
# job in macos-tests.yml where the toolchain and the libraries already are.
- name: Check the shared-contract drift gates
run: scripts/audit-refactor-health.sh --check
# The modularization moves features out of the main window's hubs. Each count may only go down.
- name: Check no hub grew past its recorded size
run: python3 scripts/ci/check-hub-ratchet.py
- name: Validate the hub ratchet
run: python3 scripts/ci/test_check_hub_ratchet.py
# The one check to mark required on main.
gate:
name: Repo Hygiene Gate
if: always()
needs: [changes, lint]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require the checks to have passed or been skipped
env:
RESULTS: ${{ join(needs.*.result, ' ') }}
run: |
set -euo pipefail
echo "job results: $RESULTS"
for result in $RESULTS; do
case "$result" in
success|skipped) ;;
*)
echo "A repo hygiene job reported '$result'." >&2
exit 1
;;
esac
done