├── .buildkite ├── pipeline-bash-tests.yml ├── pipeline-windows-tests.yml └── pipeline.yml ├── .bundle └── config ├── .cursor └── rules │ ├── bash_scripts.mdc │ ├── context.mdc │ └── powershell_scripts.mdc ├── .editorconfig ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── run-danger.yml ├── .gitignore ├── .rubocop.yml ├── .ruby-version ├── CHANGELOG.md ├── Dangerfile ├── Gemfile ├── Gemfile.lock ├── MIGRATION.md ├── Makefile ├── README.md ├── bin ├── add_host_to_ssh_known_hosts ├── add_ssh_key_to_agent ├── annotate_test_failures ├── build_and_test_pod ├── cache_cocoapods ├── cache_cocoapods_specs_repos ├── cache_repo ├── comment_on_pr ├── comment_with_dependency_diff ├── comment_with_manifest_diff ├── craft_comment_with_dependency_diff ├── download_artifact ├── dump_ruby_env_info ├── get_libc_version ├── github_api ├── hash_directory ├── hash_file ├── install_cocoapods ├── install_gems ├── install_npm_packages ├── install_swiftpm_dependencies ├── install_windows_10_sdk.ps1 ├── lint_localized_strings_format ├── lint_pod ├── merge_junit_reports ├── nvm_install ├── patch-cocoapods ├── path_aware_refreshenv.ps1 ├── pr_changed_files ├── prepare_to_publish_to_s3_params ├── prepare_windows_host_for_app_distribution.ps1 ├── publish_pod ├── publish_private_pod ├── restore_cache ├── restore_gradle_dependency_cache ├── run_swiftlint ├── save_cache ├── save_gradle_dependency_cache ├── slack_notify_pod_published ├── upload_artifact ├── upload_buildkite_test_analytics_junit ├── upload_sarif_to_github ├── validate_gemfile_lock ├── validate_gradle_wrapper ├── validate_podfile_lock ├── validate_podspec └── validate_swift_package ├── hooks └── environment ├── jar └── dependency-diff-tldr-0.0.6.jar ├── plugin.yml └── tests ├── environment.bats ├── install_swiftpm_dependencies ├── Makefile ├── README.md ├── package │ ├── .gitignore │ ├── Package.resolved │ ├── Package.swift │ ├── Sources │ │ └── Demo │ │ │ └── Demo.swift │ └── Tests │ │ └── DemoTests │ │ └── DemoTests.swift ├── package_resolved_fixtures │ └── valid.resolved ├── project │ ├── .gitignore │ ├── Makefile │ └── project.yml ├── test_scripts │ ├── run_tests_with_xcodebuild.sh │ ├── set_up_environment.sh │ ├── test_package_automatic.sh │ ├── test_package_explicit.sh │ ├── test_project_automatic.sh │ ├── test_project_explicit.sh │ ├── test_project_no_package.sh │ ├── test_workspace_and_project_automatic.sh │ ├── test_workspace_and_project_explicit.sh │ ├── test_workspace_automatic.sh │ └── test_workspace_explicit.sh ├── workspace │ ├── .gitignore │ ├── Demo.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ └── Package.resolved │ └── Makefile └── workspace_and_project │ ├── .gitignore │ ├── Demo.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved │ └── Makefile ├── pr_changed_files ├── test_all_match_patterns.sh ├── test_any_match_patterns.sh ├── test_basic_changes.sh ├── test_edge_cases.sh └── test_helpers.sh ├── test_craft_comment_with_dependency_diff.bats ├── test_install_windows_10_sdk.ps1 ├── test_prepare_windows_host_for_app_distribution.ps1 ├── test_that_all_files_are_executable.rb └── validate_get_libc_version_format /.buildkite/pipeline-bash-tests.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json 2 | --- 3 | 4 | # For convenience with the many install_swiftpm_dependencies steps, we declare agents and env for macOS in the root, then specialize the fewer remaining steps 5 | agents: 6 | queue: mac 7 | env: 8 | IMAGE_ID: xcode-15.4 9 | 10 | steps: 11 | - group: ":c: get_libc_version Tests" 12 | steps: 13 | - label: ":c: Test get_libc_version on {{matrix}}" 14 | command: ./tests/validate_get_libc_version_format 15 | agents: 16 | queue: "{{matrix}}" 17 | matrix: 18 | - android 19 | - default 20 | - mac 21 | - tumblr-metal 22 | # Could not find a way to run on Windows with the same call as the others. 23 | # See syntax in the dedicated step in pipeline-windows-tests.yml. 24 | # - windows 25 | 26 | - group: ":swift: install_swiftpm_dependencies Tests" 27 | steps: 28 | - label: ":swift: Standalone Swift Package - Autodetect" 29 | command: tests/install_swiftpm_dependencies/test_scripts/test_package_automatic.sh 30 | notify: 31 | - github_commit_status: 32 | context: "install_swiftpm_dependencies Tests: Standalone Swift Package - Autodetect" 33 | 34 | - label: ":swift: Standalone Swift Package - Explicit" 35 | command: tests/install_swiftpm_dependencies/test_scripts/test_package_explicit.sh 36 | notify: 37 | - github_commit_status: 38 | context: "install_swiftpm_dependencies Tests: Standalone Swift Package - Explicit" 39 | 40 | - label: ":xcode: Xcode Project - Autodetect" 41 | command: tests/install_swiftpm_dependencies/test_scripts/test_project_automatic.sh 42 | notify: 43 | - github_commit_status: 44 | context: "install_swiftpm_dependencies Tests: Xcode Project - Autodetect" 45 | 46 | - label: ":xcode: Xcode Project - Explicit" 47 | command: tests/install_swiftpm_dependencies/test_scripts/test_project_explicit.sh 48 | notify: 49 | - github_commit_status: 50 | context: "install_swiftpm_dependencies Tests: Xcode Project - Explicit" 51 | 52 | - label: ":xcode: Xcode Project - No Package.resolved" 53 | command: tests/install_swiftpm_dependencies/test_scripts/test_project_no_package.sh 54 | notify: 55 | - github_commit_status: 56 | context: "install_swiftpm_dependencies Tests: Xcode Project - No Package.resolved" 57 | 58 | - label: ":xcode: Xcode Workspace - Autodetect" 59 | command: tests/install_swiftpm_dependencies/test_scripts/test_workspace_automatic.sh 60 | notify: 61 | - github_commit_status: 62 | context: "install_swiftpm_dependencies Tests: Xcode Workspace - Autodetect" 63 | 64 | - label: ":xcode: Xcode Workspace - Explicit" 65 | command: tests/install_swiftpm_dependencies/test_scripts/test_workspace_explicit.sh 66 | notify: 67 | - github_commit_status: 68 | context: "install_swiftpm_dependencies Tests: Xcode Workspace - Explicit" 69 | 70 | - label: ":xcode: Xcode Workspace and Project - Autodetect" 71 | command: tests/install_swiftpm_dependencies/test_scripts/test_workspace_and_project_automatic.sh 72 | notify: 73 | - github_commit_status: 74 | context: "install_swiftpm_dependencies Tests: Xcode Workspace and Project - Autodetect" 75 | 76 | - label: ":xcode: Xcode Workspace and Project - Explicit" 77 | command: tests/install_swiftpm_dependencies/test_scripts/test_workspace_and_project_explicit.sh 78 | notify: 79 | - github_commit_status: 80 | context: "install_swiftpm_dependencies Tests: Xcode Workspace and Project - Explicit" 81 | 82 | - group: ":github: pr_changed_files Tests" 83 | steps: 84 | - label: ":github: pr_changed_files Tests - Basic Changes" 85 | command: tests/pr_changed_files/test_basic_changes.sh 86 | agents: 87 | queue: "{{matrix}}" 88 | matrix: 89 | - android 90 | - default 91 | - mac 92 | - tumblr-metal 93 | notify: 94 | - github_commit_status: 95 | context: "pr_changed_files Tests: Basic Changes" 96 | 97 | - label: ":github: pr_changed_files Tests - Any Match" 98 | command: tests/pr_changed_files/test_any_match_patterns.sh 99 | agents: 100 | queue: "{{matrix}}" 101 | matrix: 102 | - android 103 | - default 104 | - mac 105 | - tumblr-metal 106 | notify: 107 | - github_commit_status: 108 | context: "pr_changed_files Tests: Any Match" 109 | 110 | - label: ":github: pr_changed_files Tests - All Match" 111 | command: tests/pr_changed_files/test_all_match_patterns.sh 112 | agents: 113 | queue: "{{matrix}}" 114 | matrix: 115 | - android 116 | - default 117 | - mac 118 | - tumblr-metal 119 | notify: 120 | - github_commit_status: 121 | context: "pr_changed_files Tests: All Match" 122 | 123 | - label: ":github: pr_changed_files Tests - Edge Cases" 124 | command: tests/pr_changed_files/test_edge_cases.sh 125 | agents: 126 | queue: "{{matrix}}" 127 | matrix: 128 | - android 129 | - default 130 | - mac 131 | - tumblr-metal 132 | notify: 133 | - github_commit_status: 134 | context: "pr_changed_files Tests: Edge Cases" 135 | -------------------------------------------------------------------------------- /.buildkite/pipeline-windows-tests.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json 2 | --- 3 | 4 | # For convenience with the many install_swiftpm_dependencies steps, we declare agents and env for macOS in the root, then specialize the fewer remaining steps 5 | agents: 6 | queue: windows 7 | 8 | steps: 9 | - label: ":c: :windows: Test get_libc_version on Windows" 10 | command: | 11 | & "C:\Program Files\Git\bin\bash.exe" .\tests\validate_get_libc_version_format 12 | agents: 13 | queue: windows 14 | 15 | - group: ":windows: install_windows_10_sdk Tests" 16 | steps: 17 | - label: ":windows: install_windows_10_sdk Tests - Version file with valid format and version" 18 | command: | 19 | "20348" | Out-File .windows-10-sdk-version 20 | .\tests\test_install_windows_10_sdk.ps1 -ExpectedExitCode 0 21 | agents: 22 | queue: windows 23 | notify: 24 | - github_commit_status: 25 | context: "install_windows_10_sdk Tests - Version file with valid format and version" 26 | 27 | - label: ":windows: install_windows_10_sdk Tests - Version file with one new line" 28 | command: | 29 | "20348`n" | Out-File .windows-10-sdk-version 30 | .\tests\test_install_windows_10_sdk.ps1 -ExpectedExitCode 0 31 | agents: 32 | queue: windows 33 | notify: 34 | - github_commit_status: 35 | context: "install_windows_10_sdk Tests - Version file with one new line" 36 | 37 | - label: ":windows: install_windows_10_sdk Tests - Version file with more than one new line" 38 | command: | 39 | "20348`n`n" | Out-File .windows-10-sdk-version 40 | .\tests\test_install_windows_10_sdk.ps1 -ExpectedExitCode 0 41 | agents: 42 | queue: windows 43 | notify: 44 | - github_commit_status: 45 | context: "install_windows_10_sdk Tests - Version file with more than one new line" 46 | 47 | - label: ":windows: install_windows_10_sdk Tests - Version file with leading whitespaces" 48 | command: | 49 | " 19041" | Out-File .windows-10-sdk-version 50 | .\tests\test_install_windows_10_sdk.ps1 -ExpectedExitCode 0 51 | agents: 52 | queue: windows 53 | notify: 54 | - github_commit_status: 55 | context: "install_windows_10_sdk Tests - Version file with leading whitespaces" 56 | 57 | - label: ":windows: install_windows_10_sdk Tests - Version file with trailing whitespaces" 58 | command: | 59 | "18362 " | Out-File .windows-10-sdk-version 60 | .\tests\test_install_windows_10_sdk.ps1 -ExpectedExitCode 0 61 | agents: 62 | queue: windows 63 | notify: 64 | - github_commit_status: 65 | context: "install_windows_10_sdk Tests - Version file with trailing whitespaces" 66 | 67 | - label: ":windows: install_windows_10_sdk Tests - Version file with a word" 68 | command: | 69 | "not an integer" | Out-File .windows-10-sdk-version 70 | .\tests\test_install_windows_10_sdk.ps1 ` 71 | -ExpectedExitCode 1 ` 72 | -ExpectedErrorKeyphrase "Expected an integer" 73 | agents: 74 | queue: windows 75 | notify: 76 | - github_commit_status: 77 | context: "install_windows_10_sdk Tests - Version file with a word" 78 | 79 | - label: ":windows: install_windows_10_sdk Tests - Missing version file" 80 | command: | 81 | .\tests\test_install_windows_10_sdk.ps1 ` 82 | -ExpectedExitCode 1 ` 83 | -ExpectedErrorKeyphrase "No Windows 10 SDK version file found at .windows-10-sdk-version" 84 | agents: 85 | queue: windows 86 | notify: 87 | - github_commit_status: 88 | context: "install_windows_10_sdk Tests - Version file with a word" 89 | 90 | - label: ":windows: install_windows_10_sdk Tests - Version file with version number that is not in the allowed list" 91 | command: | 92 | "12345" | Out-File .windows-10-sdk-version 93 | .\tests\test_install_windows_10_sdk.ps1 ` 94 | -ExpectedExitCode 1 ` 95 | -ExpectedErrorKeyphrase "Invalid Windows 10 SDK version: 12345" 96 | agents: 97 | queue: windows 98 | notify: 99 | - github_commit_status: 100 | context: "install_windows_10_sdk Tests - Version file with version number that is not in the allowed list" 101 | 102 | - label: ":windows: prepare_windows_host_for_app_distribution Tests - Skip Windows 10 SDK Installation" 103 | command: .\tests\test_prepare_windows_host_for_app_distribution.ps1 104 | agents: 105 | queue: windows 106 | notify: 107 | - github_commit_status: 108 | context: "prepare_windows_host_for_app_distribution Tests - Skip Windows 10 SDK Installation" 109 | -------------------------------------------------------------------------------- /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json 2 | --- 3 | 4 | # For convenience with the many install_swiftpm_dependencies steps, we declare agents and env for macOS in the root, then specialize the fewer remaining steps 5 | agents: 6 | queue: mac 7 | env: 8 | IMAGE_ID: xcode-15.4 9 | 10 | steps: 11 | - label: ":sleuth_or_spy: Lint" 12 | command: make lint 13 | notify: 14 | - github_commit_status: 15 | context: Lint 16 | agents: 17 | queue: default 18 | 19 | - label: ":microscope: Test" 20 | command: make test 21 | notify: 22 | - github_commit_status: 23 | context: Test 24 | agents: 25 | queue: default 26 | 27 | - label: ":danger: Danger - PR Check" 28 | command: danger 29 | key: danger 30 | if: build.pull_request.id != null 31 | retry: 32 | manual: 33 | permit_on_passed: true 34 | notify: 35 | - github_commit_status: 36 | context: Danger - PR Check 37 | agents: 38 | queue: linter 39 | 40 | 41 | - label: ":windows: :test_tube: Run Windows Tests" 42 | command: buildkite-agent pipeline upload .buildkite/pipeline-windows-tests.yml 43 | agents: 44 | queue: upload 45 | 46 | - label: ":bash: :test_tube: Run Bash Commands Tests" 47 | command: buildkite-agent pipeline upload .buildkite/pipeline-bash-tests.yml 48 | agents: 49 | queue: upload 50 | -------------------------------------------------------------------------------- /.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_PATH: "vendor/bundle" 3 | BUNDLE_JOBS: "16" 4 | BUNDLE_SPECIFIC_PLATFORM: "false" 5 | BUNDLE_FORCE_RUBY_PLATFORM: "true" 6 | -------------------------------------------------------------------------------- /.cursor/rules/bash_scripts.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Standards and Rules to follow when editing bash scripts in @bin/ 3 | globs: bin/* 4 | --- 5 | # Shell Script Documentation and Standards 6 | 7 | ## Shell Script Standards 8 | 9 | For all bash shell scripts: 10 | - Ensure compatibility with `bash 3.2.57` 11 | - Follow `shellcheck` recommendations 12 | - Use proper error handling and input validation 13 | - Document cross-platform considerations 14 | 15 | ## Best Practices 16 | 17 | For all bash shell scripts: 18 | - Use `#!/bin/bash` as the first line of the script (a.k.a. shebang) 19 | - Use `set -e` to exit on error 20 | - Use `set -u` to error on undefined variables 21 | - Use `set -o pipefail` for proper error handling 22 | - Combine those options in a single `set -euo pipefail` command, or as part of the shebang line, as appropriate 23 | - Always use `$()` instead of backticks to execute commands 24 | - Quote all variable expansions 25 | - Use `[[ ]]` for conditional expressions 26 | - Use `local` variables in functions 27 | - Include proper error handling 28 | - Ensure the script file ends with a newline 29 | 30 | ## Bash 3.2 Compatibility 31 | 32 | For all bash shell scripts, avoid using: 33 | - Associative arrays (`declare -A`) 34 | - `mapfile`/`readarray` command 35 | - `coproc` keyword 36 | - `&>` redirection operator 37 | - Case fall-through `;& operator` 38 | - Case continue `;;& operator` 39 | - `{a..b..c}` brace expansion with step 40 | - Case-modification operators in `${parameter^}`, `${parameter,}` 41 | 42 | ## Documentation Requirements 43 | 44 | When editing or creating files: 45 | - Every shell script must have comprehensive header documentation 46 | - Documentation must include: description, usage, parameters, examples 47 | - For complex logic, provide detailed examples of different use cases 48 | - Use backticks (`) around command names, executables, and technical terms in documentation 49 | - Document all return values and exit codes 50 | - Keep documentation up to date with any changes 51 | 52 | ## Documentation Template 53 | 54 | ```bash 55 | #!/bin/bash 56 | 57 | # Script: {filename} 58 | # 59 | # Description: 60 | # [Brief description of what the script does] 61 | # 62 | # Usage: 63 | # {filename} [options] [arguments] 64 | # 65 | # Options: 66 | # [List of options/flags with descriptions] 67 | # 68 | # Arguments: 69 | # [List of positional arguments with descriptions] 70 | # 71 | # Examples: 72 | # [Provide practical examples demonstrating different use cases] 73 | # 74 | # Notes: 75 | # [Any important notes about script behavior, dependencies, or limitations] 76 | # 77 | # Returns: 78 | # [Description of exit codes and their meanings] 79 | # 80 | # Requirements: 81 | # [List of required external commands/programs] 82 | # [Any platform-specific requirements] 83 | ``` 84 | 85 | ## Cross-Platform Considerations 86 | 87 | ### For all bash shell scripts 88 | 89 | - Use POSIX-compliant syntax and commands 90 | - Ensure the script is at least compatible with macOS and Linux 91 | - Make your best effort to make the bash scripts also compatible with Windows if possible, in addition to macOS and Linux. If it's not possible, document that in the script's header documentation, and, if the features handled by the bash script would be useful and make sense for use cases where we'd still need a Windows agent, propose your help to the author of the original script to create a PowerShell counterpart of that script to provide the same functionality for when running on Windows agents. 92 | - If the script would behave differently on different platforms despite your best efforts to make it behave the same way cross-platform, document any such platform-specific behavior in the script header documentation 93 | 94 | ### For bash scripts compatible with macOS and Linux 95 | 96 | You can assume the following external commands/programs will be available on the machine the script is intended to run: 97 | 98 | - `jq` 99 | - `curl` 100 | - `aws` 101 | - `git` 102 | - `awk` 103 | - `sed` 104 | - `grep` 105 | - `sort` 106 | - `tee` 107 | - `uname` 108 | - `which` 109 | - `xargs` 110 | - `buildkite-agent` 111 | - `chmod` 112 | - `ssh-add` 113 | - `ssh-keyscan` 114 | 115 | ### For bash scripts related to iOS tools and intended to be run on macOS systems 116 | 117 | You can assume the following external commands/programs will be available on the machine where the script is run, as long as you document in the script's header documentation that the script assumes it will run on a Mac CI agent: 118 | 119 | - `bundle` 120 | - `gem` 121 | - `pod` 122 | - `ruby` 123 | 124 | ### For bash scripts related to Android tools and intended to be run on Linux systems 125 | 126 | You can assume the following external commands/programs will be available on the machine where the script is run, as long as you document in the script's header documentation that the script assumes it will run on an Android CI agent: 127 | 128 | - `gradle` 129 | - `java` 130 | 131 | ### For bash scripts related to Node.js tools and intended to be run on Linux or macOS systems 132 | 133 | You can assume the following external commands/programs will be available on the machine where the script is run, as long as you document in the script's header documentation that the script assumes it will run on a Linux CI agent and use the `automattic/nvm-buildkite-plugin` to install the correct version of Node.js: 134 | 135 | - `npm` 136 | - `node` 137 | 138 | ### For any bash script that needs to have external commands/programs available not listed above 139 | 140 | Make sure to document the list of required commands in the script's header documentation, under a "Requirements" section, with a wording like "This script needs to run on a CI agent which has the following external commands installed:". 141 | 142 | ### For bash scripts that use `shasum` or `sha256sum` directly 143 | 144 | Always suggest using our `hash_file` helper command from this repository instead of using `shasum` or `sha256sum` directly, to ensure cross-platform compatibility. 145 | For scripts using `sha1sum`, suggest to switch the implementation to use checksums provided by `hash_file` instead. 146 | 147 | ## Script Validation 148 | 149 | Before committing new scripts, ensure: 150 | - `shellcheck` passes with no warnings 151 | - Header documentation is complete 152 | - Platform requirements are documented 153 | - Helper commands are used where available 154 | -------------------------------------------------------------------------------- /.cursor/rules/context.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Provide context to the agent about this repository and how its files are used 3 | globs: 4 | --- 5 | # Context 6 | 7 | - You are a senior software engineer working at Automattic, and you are part of the Apps Infra team, responsible for our CI infrastructure, including providing tools and scripts to run on our CI agents. 8 | - You are responsible for maintaining and improving the scripts in this repository. 9 | - Our CI runs on Buildkite infrastructure, using a mix of self-hosted agents (MacMinis in our DataCenter) and Linux or Windows EC2 instances on AWS. 10 | - This repository contains a collection of bash and PowerShell scripts that are used on our CI agents to help build native iOS and Mac apps, Android apps, and cross-platform Electron apps for desktop environments. 11 | - Those scripts are made available in our CI agents via the Buildkite plugin system. The scripts in the `bin/` directory are available in the `$PATH` of all our CI jobs that use `a8c-ci-toolkit` in their pipeline steps' `plugins:` attribute. 12 | -------------------------------------------------------------------------------- /.cursor/rules/powershell_scripts.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Standards and Rules to follow when editing PowerShell scripts in @bin/ 3 | globs: *.ps1 4 | --- 5 | # PowerShell Script Documentation and Standards 6 | 7 | ## PowerShell Script Standards 8 | 9 | For all PowerShell scripts: 10 | - Ensure compatibility with Windows PowerShell 5.1 (default on Windows Server) 11 | - Follow `PSScriptAnalyzer` recommendations 12 | - Use proper error handling and input validation 13 | - Use UTF-8 encoding without BOM for script files 14 | - Document Windows-specific requirements and dependencies 15 | 16 | ## Best Practices 17 | 18 | For all PowerShell scripts: 19 | - Use `$ErrorActionPreference = "Stop"` at the beginning of scripts 20 | - Use `Set-StrictMode -Version Latest` for strict variable and function checking 21 | - Always use full cmdlet names (avoid aliases like `ls`, use `Get-ChildItem` instead) 22 | - Use proper verb-noun naming convention for functions 23 | - Quote all variable expansions in strings 24 | - Use `[CmdletBinding()]` for advanced functions 25 | - Use parameter validation attributes 26 | - Include proper error handling with try/catch blocks 27 | - Use approved PowerShell verbs (Get-Verb) 28 | - End script files with a newline 29 | - Use proper PowerShell case conventions: 30 | - PascalCase for functions, cmdlets, and parameters 31 | - camelCase for variables 32 | - UPPERCASE for constants 33 | - Check for and require Administrator privileges when needed using: 34 | 35 | ```powershell 36 | if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)) { 37 | throw "This script requires Administrator privileges" 38 | } 39 | ``` 40 | 41 | ## Documentation Requirements 42 | 43 | When editing or creating PowerShell scripts in `/bin`: 44 | - Every script must have comprehensive comment-based help 45 | - Documentation must include: synopsis, description, parameters, examples 46 | - For complex logic, provide detailed examples of different use cases 47 | - Use backticks (`) around command names, executables, and technical terms in documentation 48 | - Document all return values and terminating conditions 49 | - Keep documentation up to date with any changes 50 | - Clearly document Windows version requirements or limitations 51 | - Document required Windows features or roles 52 | 53 | ## Documentation Template 54 | 55 | ```powershell 56 | <# 57 | .SYNOPSIS 58 | Brief description of what the script does. 59 | 60 | .DESCRIPTION 61 | Detailed description of the script's purpose and functionality. 62 | 63 | .PARAMETER ParameterName 64 | Description of each parameter. 65 | 66 | .EXAMPLE 67 | Example-1 68 | Detailed description of the example. 69 | 70 | .NOTES 71 | Author: [Author name] 72 | Last Edit: [Date] 73 | Version 1.0 - Initial release 74 | Requirements: Windows PowerShell 5.1 75 | Windows Requirements: 76 | - Windows Server 2019 or later 77 | - Required Windows Features: [list features] 78 | - Required Windows Roles: [list roles] 79 | - Administrator privileges: [Yes/No] 80 | 81 | .OUTPUTS 82 | Description of the script's output. 83 | 84 | .RETURNVALUES 85 | Description of possible return values and their meanings. 86 | #> 87 | ``` 88 | 89 | ## Windows-Specific Best Practices 90 | 91 | ### Registry Operations 92 | - Always use try/catch blocks when modifying registry 93 | - Use proper registry paths (HKLM:\ instead of HKEY_LOCAL_MACHINE) 94 | - Check registry key existence before operations 95 | - Document any registry modifications in script header 96 | 97 | ### Windows Services 98 | - Use proper error handling for service operations 99 | - Check service status before operations 100 | - Handle service dependencies 101 | - Document required service account privileges 102 | 103 | ### File System Operations 104 | - Use proper path handling for Windows paths 105 | - Handle long path limitations appropriately 106 | - Use proper file system permissions checks 107 | - Handle file locks and sharing violations 108 | 109 | ### Windows Features and Roles 110 | - Document required Windows features 111 | - Check feature presence before operations 112 | - Handle feature installation failures 113 | - Document minimum Windows version requirements 114 | 115 | ### Required PowerShell Modules 116 | 117 | For any PowerShell script that needs additional modules: 118 | - Document required modules in the script's help section 119 | - Use `#Requires -Modules` statements at the start of the script 120 | - Include minimum version requirements if specific versions are needed 121 | - Document any Windows-specific module dependencies 122 | 123 | ## Script Validation 124 | 125 | Before committing PowerShell scripts, ensure: 126 | - PSScriptAnalyzer passes with no warnings 127 | - Comment-based help is complete 128 | - Windows requirements are documented 129 | - All functions have proper error handling 130 | - Variables are properly scoped 131 | - Parameters are properly validated 132 | - Administrator requirements are documented and checked if needed 133 | 134 | ## PowerShell Compatibility 135 | 136 | For all PowerShell scripts, avoid using: 137 | - PowerShell 7.x exclusive features 138 | - Deprecated cmdlets and parameters 139 | - Write-Host (use Write-Output or Write-Information instead) 140 | - Positional parameters (always use named parameters) 141 | - Global variables without explicit scope declaration 142 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | trim_trailing_whitespace = true 3 | insert_final_newline = true 4 | 5 | # PowerShell 6 | [*.ps1] 7 | indent_style = space 8 | indent_size = 2 9 | charset = utf-8 10 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | --- 3 | 4 | - [ ] I have considered if this change warrants release notes and have added them to the appropriate section in the `CHANGELOG.md` if necessary. 5 | -------------------------------------------------------------------------------- /.github/workflows/run-danger.yml: -------------------------------------------------------------------------------- 1 | name: ☢️ Trigger Danger On Buildkite 2 | 3 | on: 4 | pull_request: 5 | types: [labeled, unlabeled, ready_for_review, review_requested, review_request_removed] 6 | 7 | jobs: 8 | dangermattic: 9 | if: ${{ (github.event.pull_request.draft == false) }} 10 | uses: Automattic/dangermattic/.github/workflows/reusable-retry-buildkite-step-on-events.yml@v1.1.2 11 | with: 12 | org-slug: automattic 13 | pipeline-slug: ci-toolkit-buildkite-plugin 14 | retry-step-key: danger 15 | build-commit-sha: ${{ github.event.pull_request.head.sha }} 16 | secrets: 17 | buildkite-api-token: ${{ secrets.TRIGGER_BK_BUILD_TOKEN }} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | 4 | # IntelliJ 5 | .idea/ 6 | 7 | # Bundler 8 | vendor/ 9 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - vendor/**/* 4 | NewCops: enable 5 | 6 | Layout/LineLength: 7 | Enabled: false 8 | 9 | Metrics/AbcSize: 10 | Enabled: false 11 | 12 | Metrics/MethodLength: 13 | Enabled: false 14 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.2 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | The format of this document is inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and the project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 4 | 5 | 32 | 33 | ## Unreleased 34 | 35 | ### Breaking Changes 36 | 37 | _None._ 38 | 39 | ### New Features 40 | 41 | _None._ 42 | 43 | ### Bug Fixes 44 | 45 | _None._ 46 | 47 | ### Internal Changes 48 | 49 | _None._ 50 | 51 | ## 5.3.1 52 | 53 | ### Bug Fixes 54 | 55 | - Improve `pr_changed_files` command to run properly on all agents [#179] 56 | 57 | ## 5.3.0 58 | 59 | ### Internal Changes 60 | 61 | - Update `comment_with_manifest_diff` and `comment_with_dependency_diff` commands with extra `git fetch/pull` calls to make both of them work as expected on all CI environments. [#177] 62 | 63 | ## 5.2.0 64 | 65 | ### Internal Changes 66 | 67 | - Update `annotate_test_failures` to accept an optional `module` argument to make the `annotation context` module aware. [#175] 68 | 69 | ## 5.1.3 70 | 71 | ### Bug Fixes 72 | 73 | - Update `comment_with_manifest_diff` command to respect base branch changes prior head branch manifest generation [#173] 74 | 75 | ## 5.1.2 76 | 77 | ### Bug Fixes 78 | 79 | - Wow, can you believe this? TL;DR list from the Android dependency diff script has now colorful headers! 🎨✨ [#170] 80 | 81 | ## 5.1.1 82 | 83 | ### Bug Fixes 84 | 85 | - Make `install_gems` account for different C system libraries if `Gemfile.lock` includes `nokogiri` [#149] 86 | 87 | ## 5.1.0 88 | 89 | ### New Features 90 | 91 | - Add new `comment_with_manifest_diff` command for commenting manifest diff on PRs in Android projects [#164] 92 | 93 | ## 5.0.0 94 | 95 | ### Breaking Changes 96 | 97 | - `prepare_windows_host_for_node.ps1` has been renamed to `prepare_windows_host_for_app_distribution.ps1` [#144] 98 | - `prepare_windows_host_for_app_distribution.ps1` no longer installs Node.js. 99 | Clients should use [`nvm-buildkite-plugin`](https://github.com/Automattic/nvm-buildkite-plugin) instead [#144] 100 | 101 | ### New Features 102 | 103 | - Added new command to install Windows 10 SDK on Windows CI machines, `install_windows_10_sdk.ps1` [#144] 104 | - `prepare_windows_host_for_app_distribution.ps1` automatically installs the Windows 10 SDK if version file is found [#144] 105 | - Added new command to run `refreshenv` on Windows preserving the `PATH`, `path_aware_refreshenv.ps1` [#144] 106 | 107 | ## 4.0.0 108 | 109 | ### Breaking Changes 110 | 111 | - `pr_changed_files` command now uses exit codes by default, with stdout output requiring explicit `--stdout` flag [#151] 112 | 113 | ## 3.11.0 114 | 115 | ### New Features 116 | 117 | - Add `pr_changed_files` command to detect changes made in a Pull Request [#148] 118 | 119 | ## 3.10.1 120 | 121 | ### Bug Fixes 122 | 123 | - Do not echo three dashes at the end of restore cache script [#146] 124 | 125 | ## 3.10.0 126 | 127 | ### New Features 128 | 129 | - Add command to prepare a Windows host to run CI for a Node app [#100] 130 | 131 | ## 3.9.1 132 | 133 | ### Bug Fixes 134 | 135 | - `Dependency Cache`: Fix Dependency Cache [#138] 136 | - `run_swiftlint`: Error gracefully when `swiftlint_version` is missing in the `.swiftlint.yml` file [#139] 137 | 138 | ## 3.9.0 139 | 140 | ### New Features 141 | 142 | - `Dependency Cache`: Dependency Cache on CI per Project `[without GRADLE_RO_DEP_CACHE]` [#135] 143 | 144 | ## 3.8.0 145 | 146 | ### New Features 147 | 148 | - `lint_localized_strings_format` will bypass CocoaPods if a `Podfile.lock` is not found [#136] 149 | 150 | ## 3.7.1 151 | 152 | ### Bug Fixes 153 | 154 | - Fix logic in our `github_api` helper to auto-detecting the GitHub host [#127] [#128] 155 | - Fix not working SARIF upload on non-PR branches [#124] 156 | 157 | ## 3.7.0 158 | 159 | ### New Features 160 | 161 | - Add a script for commenting dependencies diff on PRs in Android projects [#120] 162 | - Append summarized list to dependency diff comment [#122] 163 | 164 | ### Bug Fixes 165 | 166 | - Don't require passing owner and repo names to `upload_sarif_to_github` [#121] 167 | 168 | ## 3.6.2 169 | 170 | ### Bug Fixes 171 | 172 | - Fix passing main branch name when sending sarif file [#118] 173 | 174 | ## 3.6.1 175 | 176 | ### Bug Fixes 177 | 178 | - Check for $BUILDKITE_PULL_REQUEST = false when uploading sarif file [#116] 179 | 180 | ### Internal Changes 181 | 182 | - Tooling change: Dangermattic setup [#113] 183 | 184 | ## 3.6.0 185 | 186 | ### New Features 187 | 188 | - Refactor `install_swiftpm_dependencies` to allow specifying `--workspace`/`--project`/`--use-spm` type of project explicitly, and improve implicit/automatic project type detection logic. [#105] 189 | - Introduce a command for uploading .sarif files to GitHub's registry [#111] 190 | 191 | ### Bug Fixes 192 | 193 | - Make sure `install_swiftpm_dependencies` uses the correct `Package.resolved` file (`.xcworkspace` vs `.xcodeproj/project.xcworkspace`). [#105] 194 | 195 | ## 3.5.1 196 | 197 | ### Bug Fixes 198 | 199 | - Fix an issue in `annotate_test_failures` when test cases had apostrophes in their name. [#106] 200 | 201 | ## 3.5.0 202 | 203 | ### New Features 204 | 205 | - Introduce `merge_junit_reports` script. [#103] 206 | 207 | ## 3.4.2 208 | 209 | ### Bug Fixes 210 | 211 | - Fix `hash_directory` helper, and make it portable (Mac+Linux). [#95] 212 | 213 | ## 3.4.1 214 | 215 | ### Bug Fixes 216 | 217 | - Fix for the unbound variable error seen on `run_swiftlint`. [#92] 218 | 219 | ## 3.4.0 220 | 221 | ### Internal Changes 222 | 223 | - Restrict the possible `run_swiftlint` command arguments to "--strict" and "--lenient". [#90] 224 | - Update `swiftlint_version` RegExp to check for top-level node. [#89] 225 | 226 | ## 3.3.0 227 | 228 | ### New Features 229 | 230 | - Make `publish_private_pod` support `--allow-warnings`. [#86] 231 | 232 | ### Internal Changes 233 | 234 | - Fixed Ruby, RuboCop and RSpec versions on CI. [#88] 235 | 236 | ## 3.2.2 237 | 238 | ### Bug Fixes 239 | 240 | - Fix crash in recent update of the `publish_pod` implementation — 2nd attempt. [#85] 241 | 242 | ## 3.2.1 243 | 244 | ### Bug Fixes 245 | 246 | - Fix crash in recent update of the `publish_pod` implementation. [#84] 247 | 248 | ## 3.2.0 249 | 250 | ### New Features 251 | 252 | - Make `validate_podspec` support `--allow-warnings`, `--sources=…`, `--private`, `--include-podspecs=…` and `--external-podspecs=…` flags that it will forward to the `pod lib lint` call. `--include-podspecs=*.podspec` is also now added by default if not provided explicitly. [#82] 253 | - Make `publish_pod` support `--allow-warnings` and `--synchronous` flags that it will forward to the `pod trunk push` call. This is especially useful for repos containing multiple, co-dependant podspecs to publish at the same time, to avoid issues with CDN propagation delays. [#82] 254 | 255 | ## 3.1.0 256 | 257 | ### New Features 258 | 259 | - Added `run_swiftlint` command. It will use the project's SwiftLint version defined in `.swiftlint.yml` property `swiftlint_version` to run the right SwiftLint version via Docker, reporting the warnings/errors as Buildkite annotations. 260 | 261 | ## 3.0.1 262 | 263 | ### Internal Changes 264 | 265 | - Fix an issue where `validate_podspec` and `publish_pod` fail on Xcode 15.0.1 if the pod has a dependency which targets iOS version older than 13. [#78] 266 | 267 | ## 3.0.0 268 | 269 | ### Breaking Changes 270 | 271 | - `nvm_install` utility is removed. See [MIGRATION.md](./MIGRATION.md#from-200-to-300) for details. [#73] 272 | 273 | ### Bug Fixes 274 | 275 | - Fix issue in `annotate_test_failures` where the error details' XML node could sometimes be `nil`. [#76] 276 | - Make `install_swiftpm_dependencies` use `-onlyUsePackageVersionsFromResolvedFile` flag, to prevent `Package.resolved` from being modified on CI. [#75] 277 | 278 | ### Internal Changes 279 | 280 | - Refine message in `annotate_test_failures` when configuration for optional Slack reporting is missing. [#74] 281 | 282 | ## 2.18.2 283 | 284 | ### Bug Fixes 285 | 286 | - Ensure that `install_gems` doesn't restore a macOS cache on Linux build machines. [#69] 287 | - Ensure that `install_gems` uses the correct ruby version in its `CACHE_KEY` in all cases. [#70] 288 | 289 | ## 2.18.1 290 | 291 | ### Bug Fixes 292 | 293 | - Fix an issue with `annotate_test_failures` on CI steps that are using the `parallelism` attribute. [#67] 294 | 295 | ## 2.18.0 296 | 297 | ### Bug Fixes 298 | 299 | - `annotate_test_failures` no longer prints an empty `` box when reporting a `failure` with no extra `details`. [#63] 300 | - `annotate_test_failures` is now able to remove annotations on successful step retry _even if_ the step label contains special characters. [#65] 301 | 302 | ## 2.17.0 303 | 304 | ### New Features 305 | 306 | - When `install_cocoapods` fails because `Podfile.lock` changed in CI, it now prints a diff of the changes. [#59] 307 | - Update `annotate_test_failures` to be able to send Slack Notification when there are failures. [#60] 308 | 309 | ### Bug Fixes 310 | 311 | - Fix the `annotate_test_failures` to include test cases with error nodes in the failures list. [#58] 312 | 313 | ## 2.16.0 314 | 315 | ### New Features 316 | 317 | - Add a new `github_api` utlity to make API requests to GitHub.com or Github Enterprise instances. [#51] 318 | - Extend the `comment_on_pr` utility to update or delete the existing comment. [#51] 319 | 320 | ### Bug Fixes 321 | 322 | - Prevent `annotate_test_failures` to print a misleading red error message when trying to remove previous annotation. [#50] 323 | 324 | ### Internal Changes 325 | 326 | - Added this changelog file. [#49] 327 | - Re-implement the `comment_on_pr` utility to use `curl` instead of `gh`. [#51] 328 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Check that the PR contains changes to the CHANGELOG.md file. 4 | warn('Please add an entry in the `CHANGELOG.md` file to describe the changes made by this PR') unless git.modified_files.include?('CHANGELOG.md') 5 | 6 | pr_size_checker.check_diff_size(max_size: 500) 7 | 8 | # skip remaining checks if the PR is still a Draft 9 | if github.pr_draft? 10 | message('This PR is still a Draft: some checks will be skipped.') 11 | return 12 | end 13 | 14 | labels_checker.check( 15 | do_not_merge_labels: ['do not merge'], 16 | required_labels: [] 17 | ) 18 | 19 | warn("This PR has no reviewers. Please request a review from **@\u2060Automattic/apps-infrastructure**.") unless github_utils.requested_reviewers? 20 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'danger-dangermattic', '~> 1.1' 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.7) 5 | public_suffix (>= 2.0.2, < 7.0) 6 | ast (2.4.2) 7 | claide (1.1.0) 8 | claide-plugins (0.9.2) 9 | cork 10 | nap 11 | open4 (~> 1.3) 12 | colored2 (3.1.2) 13 | cork (0.3.0) 14 | colored2 (~> 3.1) 15 | danger (9.5.0) 16 | claide (~> 1.0) 17 | claide-plugins (>= 0.9.2) 18 | colored2 (~> 3.1) 19 | cork (~> 0.1) 20 | faraday (>= 0.9.0, < 3.0) 21 | faraday-http-cache (~> 2.0) 22 | git (~> 1.13) 23 | kramdown (~> 2.3) 24 | kramdown-parser-gfm (~> 1.0) 25 | octokit (>= 4.0) 26 | terminal-table (>= 1, < 4) 27 | danger-dangermattic (1.1.2) 28 | danger (~> 9.4) 29 | danger-plugin-api (~> 1.0) 30 | danger-rubocop (~> 0.13) 31 | rubocop (~> 1.63) 32 | danger-plugin-api (1.0.0) 33 | danger (> 2.0) 34 | danger-rubocop (0.13.0) 35 | danger 36 | rubocop (~> 1.0) 37 | faraday (2.11.0) 38 | faraday-net_http (>= 2.0, < 3.4) 39 | logger 40 | faraday-http-cache (2.5.1) 41 | faraday (>= 0.8) 42 | faraday-net_http (3.3.0) 43 | net-http 44 | git (1.19.1) 45 | addressable (~> 2.8) 46 | rchardet (~> 1.8) 47 | json (2.7.2) 48 | kramdown (2.4.0) 49 | rexml 50 | kramdown-parser-gfm (1.1.0) 51 | kramdown (~> 2.0) 52 | language_server-protocol (3.17.0.3) 53 | logger (1.6.1) 54 | nap (1.1.0) 55 | net-http (0.4.1) 56 | uri 57 | octokit (9.1.0) 58 | faraday (>= 1, < 3) 59 | sawyer (~> 0.9) 60 | open4 (1.3.4) 61 | parallel (1.26.3) 62 | parser (3.3.5.0) 63 | ast (~> 2.4.1) 64 | racc 65 | public_suffix (6.0.1) 66 | racc (1.8.1) 67 | rainbow (3.1.1) 68 | rchardet (1.8.0) 69 | regexp_parser (2.9.2) 70 | rexml (3.3.7) 71 | rubocop (1.66.1) 72 | json (~> 2.3) 73 | language_server-protocol (>= 3.17.0) 74 | parallel (~> 1.10) 75 | parser (>= 3.3.0.2) 76 | rainbow (>= 2.2.2, < 4.0) 77 | regexp_parser (>= 2.4, < 3.0) 78 | rubocop-ast (>= 1.32.2, < 2.0) 79 | ruby-progressbar (~> 1.7) 80 | unicode-display_width (>= 2.4.0, < 3.0) 81 | rubocop-ast (1.32.3) 82 | parser (>= 3.3.1.0) 83 | ruby-progressbar (1.13.0) 84 | sawyer (0.9.2) 85 | addressable (>= 2.3.5) 86 | faraday (>= 0.17.3, < 3) 87 | terminal-table (3.0.2) 88 | unicode-display_width (>= 1.1.1, < 3) 89 | unicode-display_width (2.5.0) 90 | uri (0.13.1) 91 | 92 | PLATFORMS 93 | arm64-darwin-23 94 | ruby 95 | 96 | DEPENDENCIES 97 | danger-dangermattic (~> 1.1) 98 | 99 | BUNDLED WITH 100 | 2.4.22 101 | -------------------------------------------------------------------------------- /MIGRATION.md: -------------------------------------------------------------------------------- 1 | # Migration Instructions for Major Releases 2 | 3 | ## From 4.0.0 to 5.0.0 4 | 5 | * Use `prepare_windows_host_for_app_distribution.ps1` instead of `prepare_windows_host_for_node.ps1`. 6 | * This plugin no longer sets up Node.js in Windows clients, us use [`nvm-buildkite-plugin`](https://github.com/Automattic/nvm-buildkite-plugin) instead. 7 | 8 | ## From 3.0.0 to 4.0.0 9 | 10 | * In 4.0.0, the `pr_changed_files` command uses exit codes by default. 11 | To flag to replicate the previous behavior with stdout output, use the `--stdout`. 12 | 13 | ## From 2.0.0 to 3.0.0 14 | 15 | * The `nvm_install` utility has been removed in 3.0.0. Here are the detailed migration steps: 16 | - Remove all `nvm_install` calls from pipeline steps. 17 | - Add [nvm-buildkite-plugin](https://github.com/Automattic/nvm-buildkite-plugin#example) to the pipeline step yaml. 18 | - (Optional) Configure the `version` option to be the node.js version that's required by the pipeline step. The `.nvmrc` file will be used if it's not set. 19 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := default 2 | 3 | RUBY_VERSION := $(shell cat .ruby-version) 4 | 5 | default: lint test 6 | lint: buildkite-plugin-lint rubocop shellcheck 7 | test: buildkite-plugin-test rspec 8 | 9 | docker_run := docker run -t --rm -v "${PWD}"/:/plugin -w /plugin 10 | 11 | buildkite-plugin-lint: 12 | @echo ~~~ 🕵️ Plugin Linter 13 | $(docker_run) buildkite/plugin-linter --id automattic/a8c-ci-toolkit 14 | 15 | shellcheck: 16 | @echo ~~~ 🕵️ ShellCheck 17 | $(docker_run) koalaman/shellcheck $(shell find hooks bin -type f -not -name "*.ps1") --exclude=SC1071 18 | 19 | rubocop: 20 | @echo ~~~ 🕵️ Rubocop 21 | $(docker_run) ruby:$(RUBY_VERSION) /bin/bash -c \ 22 | "bundle install && bundle exec rubocop -A tests/test_that_all_files_are_executable.rb" 23 | 24 | buildkite-plugin-test: 25 | @echo ~~~ 🔬 Plugin Tester 26 | $(docker_run) buildkite/plugin-tester 27 | 28 | rspec: 29 | @echo ~~~ 🔬 Rspec 30 | $(docker_run) ruby:$(RUBY_VERSION) /bin/bash -c \ 31 | "gem install --silent rspec -v 3.13.0 && rspec tests/test_that_all_files_are_executable.rb" 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CI Toolkit Buildkite Plugin 2 | 3 | A library of commonly used commands for your CI builds. 4 | 5 | ## Example 6 | 7 | For a directory structure that looks like: 8 | 9 | ``` 10 | my-project/ 11 | ├── node_modules/ 12 | ├── package.json 13 | ├── package-lock.json 14 | 15 | ``` 16 | 17 | Add the following to your `pipeline.yml`: 18 | 19 | ```yml 20 | steps: 21 | - command: | 22 | # To persist the cache 23 | save_cache node_modules/ $(hash_file package-lock.json) 24 | 25 | # To restore the cache, if present 26 | restore_cache $(hash_file package-lock.json) 27 | 28 | plugins: 29 | - automattic/a8c-ci-toolkit#5.3.1: 30 | bucket: a8c-ci-cache # optional 31 | ``` 32 | 33 | Don't forget to verify what [the latest release](https://github.com/Automattic/a8c-ci-toolkit-buildkite-plugin/releases/latest) is and use that value instead of `5.3.1`. 34 | 35 | ## Configuration 36 | 37 | ### `bucket` (Optional, string) 38 | 39 | The name of the S3 bucket to fallback to if the `CACHE_BUCKET_NAME` environment variable is not set in the CI host. Used by `save_cache` and `restore_cache`. 40 | 41 | ## Developing 42 | 43 | To run the linter and tests: 44 | 45 | ```shell 46 | make lint 47 | make test 48 | ``` 49 | 50 | ## Contributing 51 | 52 | 1. Fork the repo 53 | 2. Make the changes 54 | 3. Run the tests 55 | 4. Commit and push your changes 56 | 5. Send a pull request 57 | 58 | ## Releasing 59 | 60 | 1. Make a PR to update references of the version number in this `README.md` and to update the `CHANGELOG.md` according to the `` to prepare it for a new release. 61 | 2. Merge the PR 62 | 3. Create a new GitHub Release, named after the new version number, and pasting the content of the `CHANGELOG.md` section corresponding to the new version as description. This will have the side effect of creating a `git tag` too, which is all we need for the new version to be available. 63 | -------------------------------------------------------------------------------- /bin/add_host_to_ssh_known_hosts: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Parameter can be just a host name, or a full http, https or git URL. 4 | URL="${1:?You need to provide an URL as first parameter}" 5 | 6 | # Use a RegEx to extract the $HOST. Match the optional `http://`, `https://` or `git@` at the start, then capture everything after that up to the next `/` or `:`. 7 | [[ $URL =~ ^(https?://|git@)?([^/:]+) ]] && HOST=${BASH_REMATCH[2]} 8 | 9 | echo "Adding ${HOST} to '~/.ssh/known_hosts'..." 10 | for ip in $(dig @8.8.8.8 "${HOST}" +short); do ssh-keyscan "${HOST}","$ip"; ssh-keyscan "$ip"; done 2>/dev/null >> ~/.ssh/known_hosts || true 11 | -------------------------------------------------------------------------------- /bin/add_ssh_key_to_agent: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | NEW_SSH_KEY=$1 4 | NEW_SSH_KEY_NAME=$2 5 | 6 | echo "--- :lock_with_ink_pen: Adding custom SSH key to agent ($NEW_SSH_KEY_NAME)" 7 | 8 | # Create key in ~/.ssh dir 9 | NEW_SSH_KEY_PATH="$HOME"/.ssh/"$NEW_SSH_KEY_NAME" 10 | echo -e "$NEW_SSH_KEY" > "$NEW_SSH_KEY_PATH" 11 | chmod 0600 "$NEW_SSH_KEY_PATH" 12 | 13 | # Add new key to Agent 14 | ssh-add "$NEW_SSH_KEY_PATH" 15 | ssh-add -l 16 | -------------------------------------------------------------------------------- /bin/annotate_test_failures: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # Usage: 6 | # annotate_test_failures [options] [junit-report-file-path] 7 | # 8 | require 'rexml/document' 9 | require 'shellwords' 10 | require 'net/http' 11 | require 'json' 12 | require 'optparse' 13 | 14 | ################### 15 | # Parse arguments 16 | ################### 17 | module_name = nil 18 | slack_channel = nil 19 | slack_webhook = ENV.fetch('SLACK_WEBHOOK', nil) # default value inferred from env var if not provided explicitly 20 | args = OptionParser.new do |opts| 21 | opts.banner = <<~HELP 22 | Usage: annotate_test_failures [junit-report-file-path] [--module MODULE] [--slack CHANNEL] [--slack-webhook URL] 23 | 24 | Annotates the Buildkite build with a summary of failed and flaky tests based on a JUnit report file (defaults to using `build/results/report.junit`). 25 | Optionally also posts the same info to a Slack channel. 26 | HELP 27 | opts.on('--module MODULE', 'The module the junit report is based on') do |v| 28 | module_name = v 29 | end 30 | opts.on('--slack CHANNEL_NAME', 'The name of the Slack channel to post the failure summary to') do |v| 31 | slack_channel = "##{v.delete_prefix('#')}" 32 | end 33 | opts.on('--slack-webhook URL', 34 | 'The Slack Webhook URL to use if --slack is used. Defaults to the value of the `SLACK_WEBHOOK` env var') do |v| 35 | slack_webhook = v 36 | end 37 | opts.on_tail('-h', '--help', 'Show this help message') do 38 | puts opts 39 | exit 40 | end 41 | end.parse! 42 | junit_path = args.first || File.join('build', 'results', 'report.junit') 43 | title = ENV.fetch('BUILDKITE_LABEL', 'Tests') 44 | 45 | unless File.exist?(junit_path) 46 | puts "JUnit file not found: #{junit_path}." 47 | # Don't fail if `.junit` file not found to avoid "hiding" the failure in the build that resulted in the that file not being created 48 | exit 0 49 | end 50 | 51 | ################### 52 | # Helper methods 53 | ################### 54 | 55 | # Here's how report.junit XML files look like for failure vs success: 56 | # 57 | # 58 | # <unknown>:0 59 | # 60 | # 61 | # Traceback (most recent call last): ... 62 | # 63 | # 64 | # 65 | class TestFailure 66 | attr_reader :classname, :name, :message, :details 67 | attr_accessor :count 68 | 69 | def initialize(node) 70 | @classname = node['classname'] 71 | @name = node['name'] 72 | failure_node = node.elements['failure'] || node.elements['error'] 73 | @message = failure_node['message'] || '' 74 | @details = failure_node.text || '' 75 | @count = 1 76 | end 77 | 78 | def to_s 79 | times = @count > 1 ? " (#{@count} times)" : '' 80 | 81 | # Do not render a code block for the failure details if there are none 82 | formatted_details = if @details.strip.empty? 83 | '' 84 | else 85 | <<~DETAILS 86 | 87 | ``` 88 | #{@details} 89 | ``` 90 | DETAILS 91 | end 92 | 93 | <<~ENTRY 94 |
#{@name} in #{@classname}#{times} 95 |
#{@message}
96 | #{formatted_details} 97 |
98 | ENTRY 99 | end 100 | 101 | def key 102 | "#{@classname}.#{@name}" 103 | end 104 | 105 | def ultimately_succeeds?(parent_node:) 106 | child_testcase_nodes = parent_node.get_elements('testcase') 107 | last_same_testcase_idx = child_testcase_nodes.rindex { |e| e['classname'] == @classname && e['name'] == @name } 108 | last_same_testcase_node = child_testcase_nodes[last_same_testcase_idx] 109 | # If last node found for that test doesn't have a or child, then test ultimately succeeded. 110 | !(last_same_testcase_node.elements['failure'] || last_same_testcase_node.elements['error']) 111 | end 112 | 113 | def ==(other) 114 | @classname == other.classname && @name == other.name && @message == other.message && @details == other.details 115 | end 116 | end 117 | 118 | # Add a test_failure into a given array, or increment its count if it already exists 119 | # 120 | def record(test_failure, into:) 121 | existing = into.find { |f| f == test_failure } 122 | if existing.nil? 123 | into.append(test_failure) 124 | else 125 | existing.count += 1 126 | end 127 | end 128 | 129 | # Given a list of failures for a given step and state, update the corresponding annotation 130 | # 131 | def update_annotation(title, module_name, list, style, state) 132 | ctx_components = [ 133 | 'test-summary', 134 | module_name || 'default-module', 135 | ENV.fetch('BUILDKITE_STEP_ID', title), 136 | ENV.fetch('BUILDKITE_PARALLEL_JOB', nil), 137 | state 138 | ] 139 | annotation_context = ctx_components.compact.join('-').downcase.gsub(/\W/, '-') 140 | if list.empty? 141 | puts "No test #{state}. Removing any previous `#{annotation_context}` Buildkite annotation if any.\n\n" 142 | system("buildkite-agent annotation remove --context #{annotation_context.shellescape} 2>/dev/null || true") 143 | else 144 | tests_count = list.map(&:key).uniq.count # Count the number of tests that failed, even if some tests might have multiple assertion failures 145 | puts "#{tests_count} test(s) #{state} (#{list.count} distinct assertion failures in total). Reporting them as a `#{annotation_context}` Buildkite #{style} annotation.\n\n" 146 | module_title = module_name ? " (#{module_name})" : '' 147 | markdown = "#### #{title}#{module_title}: #{tests_count} tests #{state} (#{list.count} distinct assertion failure(s) in total)\n\n" + list.map(&:to_s).join("\n") 148 | puts markdown 149 | system('buildkite-agent', 'annotate', markdown, '--style', style, '--context', annotation_context) 150 | end 151 | end 152 | 153 | # Given a list of failures, parse list and send a slack notification with the test names in the payload 154 | # 155 | def send_slack_notification(slack_webhook, slack_channel, title, list) 156 | failing_tests = list.map { |item| "`#{item.name}` in `#{item.classname}`" } 157 | assertion_failures_count = list.count 158 | test_text = assertion_failures_count == 1 ? 'Test' : 'Tests' 159 | 160 | slack_message_payload = { 161 | channel: slack_channel, 162 | username: "#{ENV.fetch('BUILDKITE_PIPELINE_NAME', nil)} Tests Failures", 163 | icon_emoji: ':fire:', 164 | blocks: [ 165 | { 166 | type: 'section', 167 | text: { 168 | type: 'mrkdwn', 169 | text: ":warning: *#{assertion_failures_count} #{test_text} Failed in #{ENV.fetch('BUILDKITE_PIPELINE_NAME', nil)} - #{title}*" 170 | }, 171 | accessory: { 172 | type: 'button', 173 | text: { 174 | type: 'plain_text', 175 | text: 'Build', 176 | emoji: true 177 | }, 178 | value: 'build', 179 | url: "#{ENV.fetch('BUILDKITE_BUILD_URL', nil)}##{ENV.fetch('BUILDKITE_JOB_ID', nil)}", 180 | action_id: 'button-action' 181 | } 182 | }, 183 | { 184 | type: 'divider' 185 | }, 186 | { 187 | type: 'section', 188 | text: { 189 | type: 'mrkdwn', 190 | text: "*Failing #{test_text}:*\n#{failing_tests.join("\n")}" 191 | } 192 | } 193 | ] 194 | } 195 | 196 | json_payload = JSON.generate(slack_message_payload) 197 | 198 | # Send message to Slack 199 | uri = URI(slack_webhook) 200 | response = Net::HTTP.post(uri, json_payload, 'Content-Type' => 'application/json') 201 | 202 | # Check response status 203 | if response.code == '200' 204 | puts '✅ Notification Sent!' 205 | else 206 | puts "❌ Failed to send notification. Response code: #{response.code}" 207 | end 208 | end 209 | 210 | ################### 211 | # Main 212 | ################### 213 | 214 | file = File.open(junit_path) 215 | xmldoc = REXML::Document.new(file) 216 | 217 | failures = [] 218 | flakies = [] 219 | REXML::XPath.each(xmldoc, '//testcase[failure|error]') do |node| 220 | test_failure = TestFailure.new(node) 221 | 222 | if test_failure.ultimately_succeeds?(parent_node: node.parent) 223 | record(test_failure, into: flakies) 224 | else 225 | record(test_failure, into: failures) 226 | end 227 | end 228 | 229 | update_annotation(title, module_name, failures, 'error', 'have failed') 230 | update_annotation(title, module_name, flakies, 'warning', 'were flaky') 231 | 232 | if slack_channel.nil? 233 | if slack_webhook.nil? 234 | puts '⏩ No `--slack` channel name nor `--slack-webhook`/`$SLACK_WEBHOOK` URL provided; skipping Slack notification.' 235 | else 236 | puts '⏩ No `--slack` channel name provided; skipping Slack notification.' 237 | end 238 | else 239 | send_slack_notification(slack_webhook, slack_channel, title, failures) unless failures.empty? 240 | end 241 | -------------------------------------------------------------------------------- /bin/build_and_test_pod: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | # 3 | # Any arguments passed to this script will be passed through to `fastlane`. 4 | # If no argument is passed, the `test` lane will be called by default. 5 | 6 | echo "--- :rubygems: Setting up Gems" 7 | install_gems 8 | 9 | if [ -f "Podfile.lock" ]; then 10 | echo "--- :cocoapods: Setting up Pods" 11 | install_cocoapods 12 | fi 13 | 14 | if [ -f "Example/Podfile.lock" ]; then 15 | cd Example 16 | echo "--- :cocoapods: Setting up Pods" 17 | install_cocoapods 18 | cd - 19 | fi 20 | 21 | echo "--- :test_tube: Building and Running Tests" 22 | 23 | # For some reason this fixes a failure in `lib lint` 24 | # https://github.com/Automattic/buildkite-ci/issues/7 25 | xcrun simctl list >> /dev/null 26 | 27 | bundle exec fastlane "${@:-test}" 28 | -------------------------------------------------------------------------------- /bin/cache_cocoapods: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # This file is deprecated, but is being kept around until v2.0 4 | 5 | cache_cocoapods_specs_repos 6 | -------------------------------------------------------------------------------- /bin/cache_cocoapods_specs_repos: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Update CocoaPods's master specs repo (used when you don't use the CDN) 4 | bundle exec pod repo update --verbose 5 | save_cache ~/.cocoapods "$BUILDKITE_PIPELINE_SLUG-specs-repos" --force 6 | 7 | if [ -f Podfile ]; then 8 | # Update the cache of the Pods used by the repo. 9 | # Skip if the repo doesn't have a `Podfile` (e.g. lib repo with only a `.podspec`) 10 | bundle exec pod install --verbose 11 | save_cache ~/Library/Caches/CocoaPods/ "$BUILDKITE_PIPELINE_SLUG-global-pod-cache" --force 12 | fi 13 | -------------------------------------------------------------------------------- /bin/cache_repo: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | AWS_BUCKET=$1 4 | 5 | DATE=$(date +"%Y-%m-%d") 6 | 7 | # Create the directory (and ensure it's empty) 8 | rm -rf "/tmp/$DATE" 9 | mkdir -p "/tmp/$DATE" 10 | 11 | # Download the repo 12 | git clone --mirror "$BUILDKITE_REPO" "/tmp/$DATE/$BUILDKITE_PIPELINE_SLUG.git" 13 | 14 | # Create the tarball 15 | TAR_NAME=$DATE.git.tar 16 | tar -C "/tmp/$DATE" -cvf "$TAR_NAME" "$BUILDKITE_PIPELINE_SLUG".git 17 | 18 | # Copy the file to S3 19 | aws s3 cp "$TAR_NAME" "s3://${AWS_BUCKET}/${BUILDKITE_ORGANIZATION_SLUG}/${BUILDKITE_PIPELINE_SLUG}/${TAR_NAME}" 20 | -------------------------------------------------------------------------------- /bin/comment_on_pr: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # This script is used to comment on a pull request, and must be run in a buldkite PR step. 4 | # 5 | # Usage: 6 | # [GITHUB_TOKEN=] comment_on_pr [--id ] [--if-exist update|delete] [--github-token ] 7 | # 8 | # Examples: 9 | # 10 | # # Post a new comment which can be uniquely identified by the given comment id, 11 | # # or update the existing comment that's associated with the given comment id. 12 | # # By default, the existing comment will be updated. You can choose to delete 13 | # # the existing comment by passing `--if-exist delete`. 14 | # comment_on_pr --id comment-id [--if-exist update|delete] "This is a comment" 15 | # 16 | # # Delete the comment associated with the given comment id by passing an empty comment 17 | # comment_on_pr --id comment-id [--if-exist delete] 18 | # 19 | # # Deprecated: Post a new comment to the PR 20 | # comment_on_pr "This is a comment" 21 | # 22 | # Please note the comment argument can be a path to a file containing the comment, or a markdown. 23 | 24 | # Check dependencies and print their versions for diagnosis purposes 25 | cat < jq --version 27 | $(jq --version) 28 | 29 | > curl --version 30 | $(curl --version) 31 | 32 | EOF 33 | 34 | if [[ ! "${BUILDKITE_PULL_REQUEST:-invalid}" =~ ^[0-9]+$ ]]; then 35 | echo "Error: this tool can only be called from a Buildkite PR job" 36 | exit 1 37 | fi 38 | 39 | # Default options 40 | opt_comment_id="" 41 | opt_if_exist="update" 42 | opt_github_token="${GITHUB_TOKEN:-}" 43 | 44 | # If there are two arguments and they don't start with an option, then we assume 45 | # this utility is called in the deprecated way: comment_on_pr 46 | if [[ $# == 2 && "$1" != "--"* ]]; then 47 | if [[ -f "$1" ]]; then 48 | arg_pr_comment=$(cat "$1") 49 | else 50 | arg_pr_comment="$1" 51 | fi 52 | 53 | opt_github_token=$2 54 | else 55 | while [[ "$#" -gt 0 ]]; do 56 | case $1 in 57 | --id) opt_comment_id="$2"; shift ;; 58 | --if-exist) opt_if_exist="$2"; shift ;; 59 | --github-token) opt_github_token="$2"; shift ;; 60 | --*) echo "Unknown option: $1"; exit 1 ;; 61 | *) break ;; 62 | esac 63 | shift 64 | done 65 | 66 | case "$#" in 67 | 0) 68 | arg_pr_comment="" 69 | ;; 70 | 1) 71 | if [[ -f "$1" ]]; then 72 | arg_pr_comment=$(cat "$1") 73 | else 74 | arg_pr_comment="$1" 75 | fi 76 | ;; 77 | *) echo "Error: too many arguments"; exit 1 ;; 78 | esac 79 | fi 80 | 81 | export GITHUB_TOKEN="$opt_github_token" 82 | 83 | github_user=$(basename "$(dirname "${BUILDKITE_PULL_REQUEST_REPO}")") 84 | github_repo=$(basename "${BUILDKITE_PULL_REQUEST_REPO%.git}") 85 | issues_endpoint="repos/${github_user}/${github_repo}/issues" 86 | 87 | pr_comments_file=$(mktemp) 88 | 89 | if ! github_api "$issues_endpoint/${BUILDKITE_PULL_REQUEST}/comments" --fail-with-body --output "$pr_comments_file"; 90 | then 91 | echo "Error: Failed to use the GitHub token to retreive PR comments." 92 | [[ -f "$pr_comments_file" ]] && cat "$pr_comments_file" 93 | exit 1 94 | fi 95 | 96 | # Find the existing PR comment 97 | if [[ -n "$opt_comment_id" ]]; then 98 | if ! [[ "$opt_comment_id" =~ ^[a-zA-Z0-9_-]+$ ]]; then 99 | echo "Error: comment id can only contain alphanumeric characters, dashes and underscores" 100 | exit 1 101 | fi 102 | 103 | # Do not change this variable value, it's used to identify the comments posted by this utility. 104 | comment_body_id="" 105 | existing_comment_id="$(jq --arg comment_body_id "$comment_body_id" '.[] | select(.body | contains($comment_body_id)) | .id' "$pr_comments_file")" 106 | 107 | # When comment id is provided without a comment body, delete the existing comment. 108 | if [[ -z "$arg_pr_comment" ]]; then 109 | opt_if_exist="delete" 110 | fi 111 | else 112 | comment_body_id="" 113 | existing_comment_id="" 114 | fi 115 | 116 | # Delete the existing comment if needed 117 | if [[ -n "$existing_comment_id" && "$opt_if_exist" == "delete" ]]; then 118 | echo "Delete the existing comment containing $existing_comment_id" 119 | github_api "$issues_endpoint/comments/$existing_comment_id" \ 120 | --fail-with-body -X DELETE 121 | fi 122 | 123 | # Construct the comment JSON 124 | comment_body=$(cat < "$comment_body_file" 133 | 134 | json_payload_file=$(mktemp) 135 | jq -c --null-input \ 136 | --rawfile body "$comment_body_file" \ 137 | '{ body: $body }' \ 138 | > "$json_payload_file" 139 | 140 | if [[ -n "$existing_comment_id" && "$opt_if_exist" == "update" ]]; then 141 | echo "Update the existing comment: $existing_comment_id" 142 | github_api "$issues_endpoint/comments/$existing_comment_id" \ 143 | --fail-with-body -X PATCH \ 144 | -H "Content-Type: application/json" \ 145 | --data-binary @"$json_payload_file" 146 | elif [[ -n "$arg_pr_comment" ]]; then 147 | echo "Post a new comment" 148 | github_api "$issues_endpoint/${BUILDKITE_PULL_REQUEST}/comments" \ 149 | --fail-with-body -X POST \ 150 | -H "Content-Type: application/json" \ 151 | --data-binary @"$json_payload_file" 152 | else 153 | # No comment body was given in CLI, so no new comment to post 154 | echo "No new comment to post" 155 | fi 156 | -------------------------------------------------------------------------------- /bin/comment_with_dependency_diff: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is used in a Buildkite CI pipeline to analyze and comment on dependency changes in a 4 | # GitHub pull request (PR). It compares the Gradle project dependencies and build environment between the 5 | # base branch and the head branch to identify changes caused by updates to Gradle files. 6 | # 7 | # Usage: 8 | # ./comment_with_dependency_diff 9 | # 10 | # Parameters: 11 | # : The Gradle module whose dependencies will be checked. 12 | # : The Gradle configuration to inspect for dependencies (e.g., "runtimeClasspath"). 13 | # 14 | # Steps: 15 | # 1. The script fetches the base branch of the PR and merges it into the head branch. 16 | # 2. It generates the dependency and build environment reports for both the base and head branches. 17 | # 3. The script compares these reports to detect any changes in the project's dependencies. 18 | # 4. If changes are found, the script creates a comment on the GitHub PR with the results. 19 | # 5. If no changes are detected, the script exits without posting any comment. 20 | # 21 | # Notes: 22 | # - The GitHub token must be set via the environment variable `GITHUB_TOKEN` to authenticate API requests. 23 | # - The comment on the PR will include a summary of changes and a detailed list in expandable sections. 24 | # - A jar file `dependency-tree-diff.jar` is downloaded and executed to perform the dependency diff. 25 | # 26 | # Example: 27 | # ./comment_with_dependency_diff app release 28 | # 29 | # This will analyze the `app` module with the `release` configuration and post the result to the PR. 30 | 31 | set -euo pipefail 32 | 33 | MODULE="${1:-}" 34 | CONFIGURATION="${2:-}" 35 | 36 | if [ -z "$MODULE" ] || [ -z "$CONFIGURATION" ]; then 37 | echo "Not enough arguments provided. Usage: ./comment_with_dependency_diff " 38 | exit 1 39 | fi 40 | 41 | if [ "${BUILDKITE_PULL_REQUEST:-false}" == "false" ]; then 42 | echo "This script should only be called on pull requests. Be sure to set \`if: build.pull_request.id != null\` on the step invoking it in your Buildkite pipeline" 43 | exit 2 44 | fi 45 | 46 | export DIFF_DEPENDENCIES_FOLDER="./build/reports/diff" 47 | export COMMENT_FILE="$DIFF_DEPENDENCIES_FOLDER/comment_body.txt" 48 | BASE_BRANCH_DEPENDENCIES_FILE="$DIFF_DEPENDENCIES_FOLDER/base_branch_dependencies.txt" 49 | HEAD_BRANCH_DEPENDENCIES_FILE="$DIFF_DEPENDENCIES_FOLDER/head_branch_dependencies.txt" 50 | BASE_BRANCH_BUILD_ENV_FILE="$DIFF_DEPENDENCIES_FOLDER/base_branch_build_env.txt" 51 | HEAD_BRANCH_BUILD_ENV_FILE="$DIFF_DEPENDENCIES_FOLDER/head_branch_build_env.txt" 52 | export DIFF_DEPENDENCIES_FILE="$DIFF_DEPENDENCIES_FOLDER/diff_dependencies.txt" 53 | export DIFF_BUILD_ENV_FILE="$DIFF_DEPENDENCIES_FOLDER/diff_build_env.txt" 54 | 55 | DEPENDENCY_TREE_VERSION="1.2.1" 56 | DEPENDENCY_TREE_CHECKSUM="39c50f22cd4211a3e52afec43120b7fdf90f9890" 57 | 58 | export TLDR_DIFF_DEPENDENCIES_FILE="$DIFF_DEPENDENCIES_FOLDER/tldr_diff_dependencies.txt" 59 | export TLDR_DIFF_BUILD_ENV_FILE="$DIFF_DEPENDENCIES_FOLDER/tldr_diff_build_env.txt" 60 | 61 | git config --global user.email "mobile@automattic.com" 62 | git config --global user.name "Dependency Tree Diff Tool" 63 | 64 | git fetch 65 | 66 | echo "--> Starting the check" 67 | BASE_BRANCH=$BUILDKITE_PULL_REQUEST_BASE_BRANCH 68 | echo "--> Base branch is $BASE_BRANCH" 69 | 70 | git merge "origin/$BASE_BRANCH" --no-edit 71 | 72 | if (git diff --name-status "origin/$BASE_BRANCH" | grep -E "\.gradle(\.kts)?|\.toml" --quiet); then 73 | echo "Gradle files have been changed. Looking for caused dependency changes" 74 | else 75 | echo "Gradle files haven't been changed. There is no need to run the diff" 76 | exit 0 77 | fi 78 | 79 | mkdir -p "$DIFF_DEPENDENCIES_FOLDER" 80 | 81 | echo "--> Generating dependencies to the file $HEAD_BRANCH_DEPENDENCIES_FILE" 82 | ./gradlew :"$MODULE":dependencies --configuration "$CONFIGURATION" >$HEAD_BRANCH_DEPENDENCIES_FILE 83 | 84 | echo "--> Generating build environment dependencies for the head branch" 85 | ./gradlew buildEnvironment >$HEAD_BRANCH_BUILD_ENV_FILE 86 | 87 | echo "--> Generating dependencies to the file $BASE_BRANCH_DEPENDENCIES_FILE" 88 | 89 | if ! git diff --quiet; then 90 | echo "Changes detected. Stashing and continuing with the script" 91 | git diff 92 | git stash 93 | fi 94 | 95 | git checkout "$BASE_BRANCH" 96 | git pull origin "$BASE_BRANCH" 97 | ./gradlew :"$MODULE":dependencies --configuration "$CONFIGURATION" >$BASE_BRANCH_DEPENDENCIES_FILE 98 | 99 | echo "--> Generating build environment dependencies for the base branch" 100 | ./gradlew buildEnvironment >$BASE_BRANCH_BUILD_ENV_FILE 101 | 102 | git checkout "-" 103 | 104 | echo "--> Downloading dependency-tree-diff.jar" 105 | curl -v -L "https://github.com/JakeWharton/dependency-tree-diff/releases/download/$DEPENDENCY_TREE_VERSION/dependency-tree-diff.jar" -o dependency-tree-diff.jar 106 | sha=$(sha1sum dependency-tree-diff.jar | cut -d' ' -f1) 107 | if [[ $sha != "$DEPENDENCY_TREE_CHECKSUM" ]]; then 108 | echo "dependency-tree-diff.jar file has unexpected sha1" 109 | exit 1 110 | fi 111 | chmod +x dependency-tree-diff.jar 112 | 113 | REPO_ROOT=$( dirname "$( dirname "${BASH_SOURCE[0]}" )" ) 114 | JAR_DIR="$REPO_ROOT/jar" 115 | 116 | echo "--> Running dependency-tree-diff.jar" 117 | 118 | modify_tldr_diff_file() { 119 | local file="$1" 120 | if [[ -f "$file" && -s "$file" ]]; then 121 | sed -i -e 's/^New Dependencies$/+ New Dependencies/' \ 122 | -e 's/^Upgraded Dependencies$/! Upgraded Dependencies/' \ 123 | -e 's/^Removed Dependencies$/- Removed Dependencies/' "$file" 124 | else 125 | echo "Decorating tldr didn't work: file $file does not exist or is empty." 126 | fi 127 | } 128 | 129 | # This script uses the 'dependency-diff-tldr' tool to summarize dependency differences. 130 | # Source code: https://github.com/careem/dependency-diff-tldr 131 | # Version: v0.0.6 (commit: 953a153) 132 | # To update: build from source using the repo above and `r8Jar` task 133 | ./dependency-tree-diff.jar $BASE_BRANCH_DEPENDENCIES_FILE $HEAD_BRANCH_DEPENDENCIES_FILE >$DIFF_DEPENDENCIES_FILE 134 | java -jar "$JAR_DIR/dependency-diff-tldr-0.0.6.jar" $BASE_BRANCH_DEPENDENCIES_FILE $HEAD_BRANCH_DEPENDENCIES_FILE >$TLDR_DIFF_DEPENDENCIES_FILE 135 | 136 | modify_tldr_diff_file "$TLDR_DIFF_DEPENDENCIES_FILE" 137 | 138 | ./dependency-tree-diff.jar $BASE_BRANCH_BUILD_ENV_FILE $HEAD_BRANCH_BUILD_ENV_FILE >$DIFF_BUILD_ENV_FILE 139 | java -jar "$JAR_DIR/dependency-diff-tldr-0.0.6.jar" $BASE_BRANCH_BUILD_ENV_FILE $HEAD_BRANCH_BUILD_ENV_FILE >$TLDR_DIFF_BUILD_ENV_FILE 140 | 141 | modify_tldr_diff_file "$TLDR_DIFF_BUILD_ENV_FILE" 142 | 143 | echo "--> Preparing comment for GitHub" 144 | 145 | # Check if any dependency changes were found 146 | if [ ! -s "$TLDR_DIFF_DEPENDENCIES_FILE" ] && [ ! -s "$TLDR_DIFF_BUILD_ENV_FILE" ]; then 147 | echo "No dependency changes found" 148 | comment_on_pr --id "dependency-diff" --if-exist delete 149 | exit 0 150 | fi 151 | 152 | craft_comment_with_dependency_diff 153 | 154 | comment_on_pr --id "dependency-diff" --if-exist update "$COMMENT_FILE" 155 | -------------------------------------------------------------------------------- /bin/comment_with_manifest_diff: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | MODULE="${1:-}" 6 | BUILD_VARIANT="${2:-}" 7 | 8 | # This script is used in a Buildkite CI pipeline to analyze and comment on manifest changes in a 9 | # GitHub pull request (PR). It compares the project manifest between the 10 | # base branch and the head branch to identify changes caused by updates to the project. 11 | # 12 | # Usage: 13 | # ./comment_with_manifest_diff 14 | # 15 | # Parameters: 16 | # : The project module whose manifest will be checked. 17 | # : The build variant to inspect for manifest changes (e.g., "debug", "vanillaRelease). 18 | # 19 | # Steps: 20 | # 1. It generates the manifest reports for both the base and head branch. 21 | # 2. The script compares these reports to detect any changes in the project's manifest. 22 | # 3. If changes are found, the script creates a comment on the GitHub PR with the results. 23 | # 4. If no changes are detected, the script exits without posting any comment. 24 | # 25 | # Notes: 26 | # - The GitHub token must be set via the environment variable `GITHUB_TOKEN` to authenticate API requests. 27 | # - The comment on the PR will include a detailed list of changes. 28 | # 29 | # Example: 30 | # ./comment_with_manifest_diff app release 31 | # 32 | # This will analyze the `app` module on `release` build variant and post the result to the PR. 33 | 34 | if [ -z "$MODULE" ] || [ -z "$BUILD_VARIANT" ]; then 35 | echo "Not enough arguments provided. Usage: ./comment_with_manifest_diff " 36 | exit 1 37 | fi 38 | 39 | if [ "${BUILDKITE_PULL_REQUEST:-false}" == "false" ]; then 40 | echo "This script should only be called on pull requests. Be sure to set \`if: build.pull_request.id != null\` on the step invoking it in your Buildkite pipeline" 41 | exit 2 42 | fi 43 | 44 | DIFF_MANIFEST_FOLDER="./build/reports/diff_manifest/${MODULE}/${BUILD_VARIANT}" 45 | BASE_MANIFEST_FILE="$DIFF_MANIFEST_FOLDER/base_manifest.txt" 46 | HEAD_MANIFEST_FILE="$DIFF_MANIFEST_FOLDER/head_manifest.txt" 47 | DIFF_MANIFEST_FILE="$DIFF_MANIFEST_FOLDER/diff_manifest.txt" 48 | 49 | GRADLE_TASK="${MODULE}":process"${BUILD_VARIANT^}"Manifest 50 | MANIFEST_PATH="${MODULE}/build/intermediates/merged_manifest/${BUILD_VARIANT}/process${BUILD_VARIANT^}MainManifest/AndroidManifest.xml" 51 | 52 | mkdir -p "$DIFF_MANIFEST_FOLDER" 53 | 54 | git config --global user.email "mobile@automattic.com" 55 | git config --global user.name "Android Manifest Diff Tool" 56 | 57 | git fetch 58 | 59 | echo "--> Starting Android Manifest diff check" 60 | BASE_BRANCH=$BUILDKITE_PULL_REQUEST_BASE_BRANCH 61 | echo "--> Base branch is $BASE_BRANCH" 62 | 63 | # Generate base branch manifest 64 | echo "--> Switching to base branch" 65 | git checkout "$BASE_BRANCH" 66 | git pull origin "$BASE_BRANCH" 67 | echo "--> Generating base branch manifest" 68 | ./gradlew "${GRADLE_TASK}" 69 | echo "" 70 | cp "$MANIFEST_PATH" "$BASE_MANIFEST_FILE" 71 | 72 | if ! git diff --quiet; then 73 | echo "Changes detected. Stashing and continuing with the script" 74 | git diff 75 | git stash 76 | fi 77 | 78 | # Generate head branch manifest 79 | echo "--> Switching back to head branch" 80 | git checkout "-" 81 | git merge "origin/$BASE_BRANCH" --no-edit 82 | echo "--> Generating head branch manifest" 83 | ./gradlew "${GRADLE_TASK}" 84 | echo "" 85 | cp "$MANIFEST_PATH" "$HEAD_MANIFEST_FILE" 86 | 87 | if ! git diff --quiet; then 88 | echo "Changes detected. Stashing and continuing with the script" 89 | git diff 90 | git stash 91 | fi 92 | 93 | # Generate diff 94 | echo "--> Generating manifest diff" 95 | diff -u "$BASE_MANIFEST_FILE" "$HEAD_MANIFEST_FILE" > "$DIFF_MANIFEST_FILE" || true 96 | 97 | generate_manifest_comment_body() { 98 | cat < Commenting manifest diff to GitHub" 113 | 114 | # Create or clear the comment body file 115 | COMMENT_FILE="$DIFF_MANIFEST_FOLDER/comment_body.txt" 116 | true > "$COMMENT_FILE" # 'true' used to clear the file as a no-op 117 | 118 | append_content() { 119 | local header="$1" 120 | local content="$2" 121 | local github_comment_max_size=65400 # GitHub constraint is 65536, but we decrease this value slightly to reserve for hidden comment_on_pr content and line breaks 122 | local warning_message="Content exceeds $github_comment_max_size characters. [Navigate to Buildkite build artifacts](${BUILDKITE_BUILD_URL}#${BUILDKITE_JOB_ID}) to see details." 123 | 124 | # Calculate if there's enough space for the content and header 125 | if (( ${#header} + ${#content} > github_comment_max_size )); then 126 | printf "\n%s\n%s\n" "$header" "$warning_message" > "$COMMENT_FILE" 127 | return 128 | fi 129 | 130 | printf "\n%s\n%s\n" "$header" "$content" >> "$COMMENT_FILE" 131 | } 132 | 133 | if [ -s "$DIFF_MANIFEST_FILE" ]; then 134 | append_content "## Project manifest changes for ${MODULE}" "$(generate_manifest_comment_body)" 135 | fi 136 | 137 | comment_on_pr --id "manifest-diff-${MODULE}-${BUILD_VARIANT}" --if-exist update "$COMMENT_FILE" 138 | else 139 | comment_on_pr --id "manifest-diff-${MODULE}-${BUILD_VARIANT}" --if-exist delete 140 | fi 141 | -------------------------------------------------------------------------------- /bin/craft_comment_with_dependency_diff: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script is used to craft a comment with a dependency diff and a build environment diff. 4 | # It's extracted from comment_with_dependency_diff to be tested in isolation. 5 | 6 | set -euo pipefail 7 | 8 | true > "$COMMENT_FILE" # Clear the file 9 | MAX_COMMENT_SIZE=65400 10 | current_size=0 11 | 12 | # Templates for content formatting 13 | print_header() { 14 | printf "\n## %s changes\n\n" "$1" 15 | } 16 | 17 | print_tldr() { 18 | printf "
list 19 | 20 | \`\`\`diff 21 | %s 22 | \`\`\` 23 | 24 |
25 | " "$1" 26 | } 27 | 28 | print_tree() { 29 | printf "
tree 30 | 31 | \`\`\`diff 32 | %s 33 | \`\`\` 34 | 35 |
36 | " "$1" 37 | } 38 | 39 | print_tree_too_large() { 40 | printf "\n ⚠️ %s tree is too large. [View it in Buildkite artifacts](%s#%s)\n" "$1" "$2" "$3" 41 | } 42 | 43 | # Calculate total size needed for TLDRs 44 | # This assumes that TLDRs are never too large to fit in the comment 45 | tldr_size=0 46 | for section in "Project dependencies" "Build environment"; do 47 | tldr_file="$TLDR_DIFF_DEPENDENCIES_FILE" 48 | [[ $section == "Build environment" ]] && tldr_file="$TLDR_DIFF_BUILD_ENV_FILE" 49 | 50 | if [ -s "$tldr_file" ]; then 51 | content=$(print_header "$section"; print_tldr "$(cat "$tldr_file")") 52 | tldr_size=$(( tldr_size + ${#content} )) 53 | fi 54 | done 55 | 56 | # Function to check if content would fit 57 | would_content_fit() { 58 | if (( current_size + $1 < MAX_COMMENT_SIZE )); then 59 | return 0 # Success/true - content will fit 60 | else 61 | return 1 # Failure/false - content won't fit 62 | fi 63 | } 64 | 65 | # Print content in order 66 | for section in "Project dependencies" "Build environment"; do 67 | tldr_file="$TLDR_DIFF_DEPENDENCIES_FILE" 68 | tree_file="$DIFF_DEPENDENCIES_FILE" 69 | [[ $section == "Build environment" ]] && tldr_file="$TLDR_DIFF_BUILD_ENV_FILE" && tree_file="$DIFF_BUILD_ENV_FILE" 70 | 71 | if [ -s "$tldr_file" ]; then 72 | # Print section header and TLDR 73 | print_header "$section" >> "$COMMENT_FILE" 74 | print_tldr "$(cat "$tldr_file")" >> "$COMMENT_FILE" 75 | current_size=$(wc -c < "$COMMENT_FILE") 76 | 77 | # Try to add tree if it exists 78 | if [ -s "$tree_file" ]; then 79 | tree_content=$(cat "$tree_file") 80 | content=$(print_tree "$tree_content") 81 | if would_content_fit ${#content}; then 82 | echo -e "$content" >> "$COMMENT_FILE" 83 | current_size=$(wc -c < "$COMMENT_FILE") 84 | else 85 | print_tree_too_large "$section" "$BUILDKITE_BUILD_URL" "$BUILDKITE_JOB_ID" >> "$COMMENT_FILE" 86 | fi 87 | fi 88 | fi 89 | done 90 | -------------------------------------------------------------------------------- /bin/download_artifact: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Usage 4 | # download_artifact $file_name [$destination] 5 | # 6 | # $file_name should be the name of the file - it'll automatically be combined with the current build ID to differentiate between 7 | # the same file in different jobs. It'll be identical to the `basename` of whatever you passed to `store_artifact`. 8 | # 9 | # $destination is an optional argument – by default, `download_artifact` will download the artifact to the present working directory, 10 | # but if you'd like it stored elsewhere, just pass the path as the second argument. 11 | 12 | ARTIFACT_FILE_NAME=${1-} 13 | 14 | if [ -z "$ARTIFACT_FILE_NAME" ]; then 15 | echo "You must pass the name of the file you want to download" 16 | exit 1 17 | fi 18 | 19 | BUCKET=${ARTIFACTS_S3_BUCKET-} 20 | 21 | if [ -z "$BUCKET" ]; then 22 | echo "You must pass set the \`ARTIFACTS_S3_BUCKET\` environment variable with the S3 bucket you'd like to use" 23 | exit 2 24 | fi 25 | 26 | OUTPUT_PATH=${2-.} 27 | 28 | KEY="$BUILDKITE_BUILD_ID/$ARTIFACT_FILE_NAME" 29 | 30 | # If the bucket has transfer acceleration enabled, use it! 31 | ACCELERATION_STATUS=$(aws s3api get-bucket-accelerate-configuration --bucket "$BUCKET" | jq '.Status' -r || true) 32 | 33 | if [ "$ACCELERATION_STATUS" = "Enabled" ]; then 34 | echo "Downloading with transfer acceleration" 35 | aws s3 cp "s3://$BUCKET/$KEY" "$OUTPUT_PATH" --endpoint-url https://s3-accelerate.amazonaws.com 36 | else 37 | aws s3 cp "s3://$BUCKET/$KEY" "$OUTPUT_PATH" 38 | fi 39 | 40 | -------------------------------------------------------------------------------- /bin/dump_ruby_env_info: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | echo "--- :ruby: Ruby environment info dump ${1:-}" 4 | 5 | if [[ -f .ruby-version ]]; then 6 | cat .ruby-version 7 | else 8 | echo 'Could not find .ruby-version in current folder' 9 | fi 10 | 11 | set -x # debug mode: print the commands we call 12 | 13 | which ruby 14 | ruby --version 15 | 16 | which gem 17 | gem --version 18 | 19 | which bundle 20 | gem list bundler 21 | bundle version 22 | 23 | set +x # exit debug mode 24 | -------------------------------------------------------------------------------- /bin/get_libc_version: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # 4 | # Returns the system's libc version, or a close proxy of it. 5 | # Useful for commands like install_gems which use a number of system information to compute the key for the cache they lookup. 6 | # 7 | 8 | VERBOSE=${1:-false} 9 | 10 | if [ "$VERBOSE" = true ]; then 11 | echo "~~~ :desktop_computer: Check OSTYPE" 12 | fi 13 | os_type="${OSTYPE:?Error: OSTYPE environment variable not found}" 14 | if [ "$VERBOSE" = true ]; then 15 | echo "OSTYPE is $os_type" 16 | fi 17 | 18 | if [ "$VERBOSE" = true ]; then 19 | echo "~~~ :c: Check libc version" 20 | fi 21 | 22 | if [[ "$os_type" == "linux"* ]]; then 23 | # Different Linux distributions use different libc implementations (e.g., glibc, musl), so we check explicitly. 24 | # We use `ldd --version` because `ldd` is part of the standard dynamic linker tools and reports the libc version it is linked against. 25 | version=$(ldd --version | head -n 1 | grep -o "[0-9][0-9]*\.[0-9][0-9]*" || echo "unknown") 26 | echo "linux-$version" 27 | elif [[ "$os_type" == "darwin"* ]]; then 28 | # macOS does not have a standalone libc we can check. 29 | # Instead, it has libSystem, which includes a BSD-based libc, libpthread, and other core libraries. 30 | # Since libSystem is tied to macOS releases and updated with the OS, 31 | # checking the macOS version with `sw_vers` is a good approximation. 32 | version=$(sw_vers -productVersion 2>/dev/null || echo "unknown") 33 | echo "macos-$version" 34 | elif [[ "$os_type" == "msys"* ]] || [[ "$os_type" == "cygwin"* ]] || [[ "$os_type" == "Windows_NT" ]]; then 35 | # Windows does not have a libc. 36 | # The closest thing is the MSVCRT (Microsoft C Runtime) but that varies by 37 | # Windows version. 38 | # Next up, we have the OS version, which is what we are using here. 39 | version=$(powershell -Command "[System.Environment]::OSVersion.Version.ToString()" 2>/dev/null || echo "unknown") 40 | echo "win-$version" 41 | else 42 | echo "unknown" 43 | fi 44 | -------------------------------------------------------------------------------- /bin/github_api: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Similar to `gh api `, this utility makes API calls to GitHub. 4 | # 5 | # ENVIRONMENT VARIABLES: 6 | # GITHUB_TOKEN: Required. The GitHub token to use for authentication. 7 | # GH_HOST: Optional. The GitHub host to use. We'll figure out the host from `BUILDKITE_REPO` env var if not set. 8 | # 9 | # Usage: 10 | # github_api [curl-options] 11 | # 12 | # Example: 13 | # github_api user # Send API request and print JSON response 14 | # github_api user --head # Make a HEAD request and print response headers 15 | 16 | if [[ -z "${GITHUB_TOKEN:-}" ]]; then 17 | echo "Missing environment variable GITHUB_TOKEN" 18 | exit 1 19 | fi 20 | 21 | github_endpoint="$1" 22 | shift 23 | 24 | github_host=${GH_HOST:-} 25 | if [[ -z "${github_host}" ]]; then 26 | if [[ "${BUILDKITE_REPO:-}" =~ ^(https?://|git@)([^/:]+)[:/] ]]; then 27 | github_host="${BASH_REMATCH[2]}" 28 | else 29 | github_host=github.com 30 | fi 31 | fi 32 | 33 | if [[ "$github_host" == "github.com" ]]; then 34 | url="https://api.github.com/$github_endpoint" 35 | else 36 | url="https://${github_host}/api/v3/$github_endpoint" 37 | fi 38 | 39 | curl -H "Authorization: Bearer ${GITHUB_TOKEN}" -H "Accept: application/vnd.github+json" -s "$@" "$url" 40 | exit $? 41 | -------------------------------------------------------------------------------- /bin/hash_directory: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | DIRECTORY_PATH=$1 4 | 5 | if [ -z "$1" ]; then 6 | echo "You must pass a directory name to hash" 7 | exit 1 8 | fi 9 | 10 | # `shasum` is available on only macOS 11 | if command -v shasum &> /dev/null; then 12 | sha_command=(shasum -a 256) 13 | else 14 | sha_command=(sha256sum) 15 | fi 16 | 17 | # - Find all files in the given directory 18 | # - Run `sha256sum` on each file found – the `+` flag does it in parallel for a huge speed boost. 19 | # - Sort the files by filename for deterministic hashing 20 | # - Take the hash of all of the output hashes (and file paths) 21 | find "${DIRECTORY_PATH%/}" -type f -exec "${sha_command[@]}" "{}" \+ | sort -k 2 | "${sha_command[@]}" | cut -f1 -d " " 22 | -------------------------------------------------------------------------------- /bin/hash_file: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | if [ -z "${1:-}" ]; then 4 | echo "You must pass a filename to hash" 5 | exit 1 6 | fi 7 | 8 | # `shasum` is available on only macOS 9 | if command -v shasum &> /dev/null; then 10 | sha_command=(shasum -a 256) 11 | else 12 | sha_command=(sha256sum) 13 | fi 14 | 15 | # Both `shasum` and `sha256sum` will print the hash and the file name (`$1`). 16 | # We only care about the hash, so we use `cut` to extract it. 17 | "${sha_command[@]}" "$1" | cut -f1 -d " " 18 | -------------------------------------------------------------------------------- /bin/install_cocoapods: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Start by restoring specs repo and pod cache: 4 | # - The specs repo cache holds all of the Podspec files. This avoids having download them all from the CDN. 5 | # - The pod cache holds the downloaded Pod source files. This avoids having to check them out again. 6 | restore_cache "$BUILDKITE_PIPELINE_SLUG-specs-repos" 7 | restore_cache "$BUILDKITE_PIPELINE_SLUG-global-pod-cache" 8 | 9 | PODFILE_HASH=$(hash_file Podfile.lock) 10 | LOCAL_CACHE_KEY="$BUILDKITE_PIPELINE_SLUG-local-pod-cache-$PODFILE_HASH" 11 | 12 | # Restore the local `Pods` directory based on the `Podfile.lock` contents 13 | restore_cache "$LOCAL_CACHE_KEY" 14 | 15 | # If the `pod check` plugin is installed, use it to determine whether or not to install Pods at all 16 | # If it's not installed (or if it fails), we'll try to install Pods. 17 | # If that fails, it may be due to an out-of-date repo. We can use `--repo-update` to try to resolve this automatically. 18 | if bundle exec pod plugins installed | grep -q check; then 19 | bundle exec pod check || bundle exec pod install || bundle exec pod install --repo-update --verbose 20 | else 21 | bundle exec pod install || bundle exec pod install --repo-update --verbose 22 | fi 23 | 24 | # Check that `Podfile.lock` was unchanged by `pod install`. If it was, it means 25 | # the lockfile might have been inadvertently changed. 26 | 27 | # `shasum` is available only on macOS 28 | if command -v shasum &> /dev/null; then 29 | sha_command='shasum -a 256' 30 | else 31 | sha_command='sha256sum' 32 | fi 33 | 34 | function lockfile_error () { 35 | message=$(cat <See diff 39 | 40 | \`\`\`diff 41 | $(git diff -- Podfile.lock) 42 | \`\`\` 43 | 44 | EOF 45 | ) 46 | 47 | echo "$message" 48 | 49 | buildkite-agent annotate "$message" --style 'error' --context 'ctx-error' 50 | } 51 | trap lockfile_error ERR 52 | 53 | echo "Checking that Podfile.lock was not modified by 'pod install'" 54 | # Notice the two spaces as per shasum/sha256sum output 55 | echo "${PODFILE_HASH} Podfile.lock" | $sha_command --check --status 56 | 57 | # Remove trap for the lockfile error now that we've done the check. 58 | trap - ERR 59 | 60 | # If this is the first time we've seen this particular hash of `Podfile.lock`, create a cache entry for future use 61 | save_cache Pods "$LOCAL_CACHE_KEY" 62 | -------------------------------------------------------------------------------- /bin/install_gems: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | PLATFORM=$(uname -s) 4 | ARCHITECTURE=$(uname -m) 5 | RUBY_VERSION=$(rbenv version-name 2>/dev/null || cat .ruby-version) 6 | GEMFILE_HASH=$(hash_file Gemfile.lock) 7 | # Many of our projects require nokogiri, which may result in issues if the 8 | # cache is built on an environment with a different libc version than the one 9 | # where it is downloaded. To avoid issues, add the libc version to the cache 10 | # key if the Gemfile.lock includes nokogiri. 11 | if grep -q "nokogiri" Gemfile.lock; then 12 | LIBC_VERSION="-libc$("$(dirname "${BASH_SOURCE[0]}")"/get_libc_version)" 13 | else 14 | LIBC_VERSION='' 15 | fi 16 | CACHEKEY="$BUILDKITE_PIPELINE_SLUG-$PLATFORM-$ARCHITECTURE$LIBC_VERSION-ruby$RUBY_VERSION-$GEMFILE_HASH" 17 | 18 | restore_cache "$CACHEKEY" 19 | 20 | # Install the same Bundler version as the one in the Gemfile.lock. 21 | # This should prevent runtime issues where Ruby "gets confused" on which Bundler version to use. 22 | # See https://github.com/Automattic/a8c-ci-toolkit-buildkite-plugin/issues/16 for more details. 23 | GEMFILE_LOCK_BUNDLER_VERSION=$(sed -n -e '/BUNDLED WITH/{n;s/ *//p;}' Gemfile.lock) 24 | # The command will fail if the `--version` parameter is empty; we can omit checking for that ourselves. 25 | gem install bundler --version "$GEMFILE_LOCK_BUNDLER_VERSION" 26 | 27 | bundle install 28 | 29 | # If this is the first time we've seen this particular cache key, save it for the future 30 | save_cache vendor/bundle "$CACHEKEY" 31 | -------------------------------------------------------------------------------- /bin/install_npm_packages: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Ensure package.json is present 4 | if [ ! -f package.json ]; then 5 | echo "No valid package.json file found" 6 | exit 1 7 | fi 8 | 9 | # Ensure package-lock.json is present 10 | if [ ! -f package-lock.json ]; then 11 | echo "No valid package-lock.json file found" 12 | exit 1 13 | fi 14 | 15 | PLATFORM=$(uname -s) 16 | ARCHITECTURE=$(uname -m) 17 | NODE_VERSION=$(node --version) 18 | PACKAGE_HASH=$(hash_file package-lock.json) 19 | CACHEKEY="$BUILDKITE_PIPELINE_SLUG-$PLATFORM-$ARCHITECTURE-node$NODE_VERSION-$PACKAGE_HASH" 20 | 21 | restore_cache "$CACHEKEY" 22 | 23 | npm install 24 | 25 | save_cache node_modules/ "$CACHEKEY" 26 | -------------------------------------------------------------------------------- /bin/install_swiftpm_dependencies: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | print_usage() { 4 | echo "Usage:" 5 | echo " Using the \`Package.resolved\` of an Xcode \`.xworkspace\`:" 6 | echo " ${0##*/} --workspace PATH" 7 | echo " Using the \`Package.resolved\` of an Xcode \`.xcodeproj\`:" 8 | echo " ${0##*/} --project PATH" 9 | echo " Using the \`Package.resolved\` at the root, managed by \`swift package\` instead of Xcode:" 10 | echo " ${0##*/} --use-spm" 11 | } 12 | 13 | # Parse input parameters 14 | XCWORKSPACE_PATH= 15 | XCODEPROJ_PATH= 16 | USE_SPM=false 17 | 18 | case ${1:-} in 19 | --workspace) 20 | XCWORKSPACE_PATH="${2:?you need to provide a value after the --workspace option}" 21 | shift; shift 22 | ;; 23 | --project) 24 | XCODEPROJ_PATH=${2:?you need to provide a value after the --project option} 25 | shift; shift 26 | ;; 27 | --use-spm) 28 | USE_SPM=true 29 | shift 30 | ;; 31 | -h|--help) 32 | print_usage 33 | exit 0 34 | ;; 35 | esac 36 | 37 | if [[ "$#" -gt 0 ]]; then 38 | echo "Unexpected extra arguments: $*" >&2 39 | print_usage 40 | exit 2 41 | fi 42 | 43 | SPM_CACHE_LOCATION="${HOME}/Library/Caches/org.swift.swiftpm" 44 | 45 | # Try to guess what to do if no option provided explicitly 46 | if [[ -z "${XCWORKSPACE_PATH}" && -z "${XCODEPROJ_PATH}" && "${USE_SPM}" != "true" ]]; then 47 | echo "No \`--workspace\`, \`--project\` or \`--use-spm\` flag provided. Trying to guess the correct one to use..." 48 | 49 | shopt -s nullglob 50 | FOUND_ROOT_WORKSPACE_PATHS=(*.xcworkspace) 51 | FOUND_ROOT_PROJECT_PATHS=(*.xcodeproj) 52 | 53 | if [[ ${#FOUND_ROOT_WORKSPACE_PATHS[@]} -eq 1 && -d "${FOUND_ROOT_WORKSPACE_PATHS[0]}" && ! -f "Package.swift" ]]; then 54 | XCWORKSPACE_PATH="${FOUND_ROOT_WORKSPACE_PATHS[0]}" 55 | echo " --> Found a single \`.xcworkspace\` file, and no \`Package.swift\` in the root of the repo." 56 | echo " Defaulting to \`--workspace \"${XCWORKSPACE_PATH}\"\`." 57 | elif [[ ${#FOUND_ROOT_WORKSPACE_PATHS[@]} -eq 0 && ${#FOUND_ROOT_PROJECT_PATHS[@] } -eq 1 && ! -f "Package.swift" ]]; then 58 | XCODEPROJ_PATH="${FOUND_ROOT_PROJECT_PATHS[0]}" 59 | echo " --> Found an \`.xcodeproj\`, and no \`Package.swift\` nor \`.xcworkspace\`in the root of the repo." 60 | echo " Defaulting to \`--project \"${XCODEPROJ_PATH}\`" 61 | elif [[ ${#FOUND_ROOT_WORKSPACE_PATHS[@]} -eq 0 && ${#FOUND_ROOT_PROJECT_PATHS[@] } -eq 0 && -f "Package.swift" ]]; then 62 | echo " --> Found a \`Package.swift\`, and no \`.xcworkspace\` or \`.xcodeproj\` in the root of the repo." 63 | echo " Defaulting to \`--use-spm\`" 64 | USE_SPM=true 65 | else 66 | echo " -!- No valid --workspace, --project or --use-spm flag provided, and cannot guess which one to use either, so aborting." 67 | echo " Please call $0 with an explicit \`--workspace PATH\`, \`--project PATH\` or \`--use-spm\` flag to disambiguate." 68 | exit 1 69 | fi 70 | fi 71 | 72 | # Find where Package.resolved is located so we can compute the cache key from its hash 73 | PACKAGE_RESOLVED_LOCATION= 74 | if [[ -n "${XCWORKSPACE_PATH}" ]]; then 75 | PACKAGE_RESOLVED_LOCATION="${XCWORKSPACE_PATH%/}/xcshareddata/swiftpm/Package.resolved" 76 | elif [[ -n "${XCODEPROJ_PATH}" ]]; then 77 | PACKAGE_RESOLVED_LOCATION="${XCODEPROJ_PATH%/}/project.xcworkspace/xcshareddata/swiftpm/Package.resolved" 78 | elif [[ "${USE_SPM}" == "true" ]]; then 79 | PACKAGE_RESOLVED_LOCATION="Package.resolved" 80 | fi 81 | 82 | if [[ ! -f "${PACKAGE_RESOLVED_LOCATION}" ]]; then 83 | echo "Unable to find \`Package.resolved\` file (${PACKAGE_RESOLVED_LOCATION:-unable to guess path})" 84 | exit 1 85 | fi 86 | 87 | # Restore SPM cache if it's available 88 | echo "~~~ Restoring SPM cache if available" 89 | PACKAGE_RESOLVED_HASH=$(hash_file "${PACKAGE_RESOLVED_LOCATION}") 90 | CACHE_KEY="${BUILDKITE_PIPELINE_SLUG}-spm-cache-${PACKAGE_RESOLVED_HASH}" 91 | 92 | mkdir -p "${SPM_CACHE_LOCATION}" 93 | cd "${SPM_CACHE_LOCATION}" 94 | restore_cache "${CACHE_KEY}" 95 | cd - 96 | 97 | # This will let Xcode use the system SSH config for downloading packages 98 | sudo defaults write com.apple.dt.Xcode IDEPackageSupportUseBuiltinSCM YES 99 | 100 | # Trust all GitHub.com and BitBucket.org keys – this allows checking out dependencies via SSH 101 | add_host_to_ssh_known_hosts bitbucket.org 102 | add_host_to_ssh_known_hosts github.com 103 | 104 | # Resolve the packages using the correct method 105 | if [[ -d "${XCWORKSPACE_PATH}" ]]; then 106 | echo "~~~ Resolving Swift Packages with \`xcodebuild\`" 107 | echo "Using -workspace \"${XCWORKSPACE_PATH}\"" 108 | # Note: we use `-list` as a workaround because otherwise `xcodebuild` complains that using `-workspace` requires to also provide the `-scheme` option 109 | # (despite the help page of `xcodebuild` suggesting that it should work without `-scheme`). Since the dependency resolution doesn't really depend on the scheme 110 | # and we don't want to have to provide or guess it, using `-list` instead stops making `xcodebuild` complain about `-workspace` not being used in conjunction 111 | # with `-scheme` (even if in practice we don't care about the scheme list it returns) 112 | xcodebuild -workspace "${XCWORKSPACE_PATH}" -resolvePackageDependencies -onlyUsePackageVersionsFromResolvedFile -list 113 | elif [[ -d "${XCODEPROJ_PATH}" ]]; then 114 | echo "~~~ Resolving Swift Packages with \`xcodebuild\`" 115 | echo "Using -project \"${XCODEPROJ_PATH}\"" 116 | xcodebuild -project "${XCODEPROJ_PATH}" -resolvePackageDependencies -onlyUsePackageVersionsFromResolvedFile 117 | elif [[ "${USE_SPM}" == "true" ]]; then 118 | echo "~~~ Resolving packages with \`swift package\`" 119 | swift package resolve 120 | fi 121 | 122 | # `checkouts` can be removed because the system can quickly generate them 123 | # instead of needing to download them in the cache each time. 124 | # 125 | # `artifacts` should be removed because it causes issues when downloading 126 | # certain packages to have the artifacts already present after extracting 127 | # cache 128 | echo "~~~ Cleaning up cache files before saving cache" 129 | rm -rf "${SPM_CACHE_LOCATION}/checkouts" "${SPM_CACHE_LOCATION}/artifacts" 130 | echo "Done. Removed checkouts and artifacts subfolders from $SPM_CACHE_LOCATION" 131 | 132 | # If this is the first time we've seen this particular cache key, save it for the future 133 | echo "~~~ Saving SPM Cache" 134 | save_cache "${SPM_CACHE_LOCATION}" "${CACHE_KEY}" false --use_relative_path_in_tar 135 | -------------------------------------------------------------------------------- /bin/install_windows_10_sdk.ps1: -------------------------------------------------------------------------------- 1 | # Install the Windows 10 SDK and Visual Studio Build Tools using the value in .windows-10-sdk-version. 2 | # 3 | # The expected .windows-10-sdk-version format is an integer representing a valid SDK component id. 4 | # The list of valid component ids can be found at 5 | # https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools?view=vs-2022 6 | # 7 | # Example: 8 | # 9 | # 20348 10 | 11 | param ( 12 | [switch]$DryRun = $false 13 | ) 14 | 15 | # Stop script execution when a non-terminating error occurs 16 | $ErrorActionPreference = "Stop" 17 | 18 | Write-Output "--- :windows: Installing Windows 10 SDK and Visual Studio Build Tools" 19 | 20 | # See list at https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools?view=vs-2022 21 | $allowedVersions = @( 22 | "20348", 23 | "19041", 24 | "18362", 25 | "17763", 26 | "17134", 27 | "16299", 28 | "15063", 29 | "14393" 30 | ) 31 | 32 | $windowsSDKVersionFile = ".windows-10-sdk-version" 33 | if (-not (Test-Path $windowsSDKVersionFile)) { 34 | Write-Output "[!] No Windows 10 SDK version file found at $windowsSDKVersionFile." 35 | exit 1 36 | } 37 | 38 | $windows10SDKVersion = (Get-Content -TotalCount 1 $windowsSDKVersionFile).Trim() 39 | 40 | if ($windows10SDKVersion -notmatch '^\d+$') { 41 | Write-Output "[!] Invalid version file format." 42 | Write-Output "Expected an integer, got: '$windows10SDKVersion'" 43 | exit 1 44 | } 45 | 46 | if ($allowedVersions -notcontains $windows10SDKVersion) { 47 | Write-Output "[!] Invalid Windows 10 SDK version: $windows10SDKVersion" 48 | Write-Output "Allowed versions are:" 49 | foreach ($version in $allowedVersions) { 50 | Write-Output "- $version" 51 | } 52 | Write-Output "More info at https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools?view=vs-2022" 53 | exit 1 54 | } 55 | 56 | Write-Output "Will attempt to set up Windows 10 ($windows10SDKVersion) SDK and Visual Studio Build Tools..." 57 | 58 | if ($DryRun) { 59 | Write-Output "Running in dry run mode, finishing here." 60 | exit 0 61 | } 62 | 63 | # Download the Visual Studio Build Tools Bootstrapper 64 | Write-Output "~~~ Downloading Visual Studio Build Tools..." 65 | 66 | $buildToolsPath = ".\vs_buildtools.exe" 67 | 68 | Invoke-WebRequest ` 69 | -Uri https://aka.ms/vs/17/release/vs_buildtools.exe ` 70 | -OutFile $buildToolsPath 71 | 72 | If (-not (Test-Path $buildToolsPath)) { 73 | Write-Output "[!] Failed to download Visual Studio Build Tools" 74 | Exit 1 75 | } else { 76 | Write-Output "Successfully downloaded Visual Studio Build Tools at $buildToolsPath." 77 | } 78 | 79 | # Install the Windows SDK and other required components 80 | Write-Output "~~~ Installing Visual Studio Build Tools..." 81 | Start-Process ` 82 | -FilePath $buildToolsPath ` 83 | -ArgumentList "--quiet --wait --add Microsoft.VisualStudio.Component.Windows10SDK.$windows10SDKVersion" ` 84 | -NoNewWindow ` 85 | -Wait 86 | 87 | # Check if the installation was successful in file system 88 | $windowsSDKsRoot = "C:\Program Files (x86)\Windows Kits\10\bin" 89 | $sdkPath = "$windowsSDKsRoot\10.0.$windows10SDKVersion.0\x64" 90 | If (-not (Test-Path $sdkPath)) { 91 | Write-Output "[!] Failed to install Windows 10 SDK: Could not find SDK at $sdkPath." 92 | If (-not (Test-Path $windowsSDKsRoot)) { 93 | Write-Output "[!] Expected $windowsSDKsRoot to exist, but it does not." 94 | } else { 95 | Write-Output " Found:" 96 | Get-ChildItem -Path $windowsSDKsRoot | ForEach-Object { Write-Output " - $windowsSDKsRoot\$_" } 97 | } 98 | Exit 1 99 | } 100 | 101 | Write-Output "Visual Studio Build Tools + Windows 10 ($windows10SDKVersion) SDK installation completed. SDK path: $sdkPath." 102 | Write-Output "Windows 10 SDK path: $sdkPath." 103 | 104 | Write-Output "~~~ Cleaning up..." 105 | Remove-Item -Path $buildToolsPath 106 | Write-Output "All cleaned up." 107 | -------------------------------------------------------------------------------- /bin/lint_localized_strings_format: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Attempts to generate the frozen `.strings` file to gets sent to GlotPress 4 | # after a new release code freeze. 5 | # 6 | # If the process fails, annotates the build with the errors. 7 | 8 | # The strings generation happens via Fastlane, so we need to install the 9 | # Ruby gems. 10 | echo "--- :rubygems: Setting up Gems" 11 | install_gems 12 | 13 | # The strings generation also picks up files from our first party libraries, 14 | # some of which are installed via CocoaPods 15 | echo "--- :cocoapods: Setting up Pods" 16 | if [[ -f Podfile.lock ]]; then 17 | install_cocoapods 18 | else 19 | echo "CocoaPods setup not detected. Skipping CocoaPods installation..." 20 | fi 21 | 22 | echo "--- :sleuth_or_spy: Lint Apple Localized Strings Format" 23 | # A next step improvement is to move the logs management within the release 24 | # toolkit 25 | LOGS=logs.txt 26 | set +e 27 | set -o pipefail 28 | if [[ $# -eq 0 ]]; then 29 | # If no argument provided, update $@ with default values 30 | set -- generate_strings_file_for_glotpress skip_commit:true 31 | fi 32 | bundle exec fastlane "$@" | tee $LOGS 33 | EXIT_CODE=$? 34 | set -e 35 | 36 | if [[ $EXIT_CODE -ne 0 ]]; then 37 | # Strings generation finished with errors, extract the errors in an easy-to-find section 38 | echo "+++ :x: Strings Generation Failed" 39 | ERRORS_FILE=errors.txt 40 | printf "Found errors when trying to run \`genstrings\` to generate the \`.strings\` files from \`{NS,App}LocalizedStrings\` calls:\n\n" | tee $ERRORS_FILE 41 | # Print the errors inline. 42 | # 43 | # Notice the second `sed` call that removes the ANSI escape sequences that 44 | # Fastlane uses to color the output. We need to do this at this point to 45 | # account for Fastlane printing the errors multiple times (inline and in the 46 | # end of the lane summary) but with different escape sequences. Without it, 47 | # we would get multiple hits for the same error. 48 | # 49 | # See discussion at 50 | # https://github.com/wordpress-mobile/WordPress-iOS/pull/19553#discussion_r1017743950 51 | cat $LOGS \ 52 | | sed -ne 's/\[.*\].*genstrings: error: /- /p' \ 53 | | sed -e $'s/\x1b\[[0-9;]*m//g' \ 54 | | sort \ 55 | | uniq \ 56 | | tee -a $ERRORS_FILE 57 | # Annotate the build with the errors 58 | cat $ERRORS_FILE | buildkite-agent annotate --style error --context genstrings 59 | fi 60 | 61 | exit $EXIT_CODE 62 | -------------------------------------------------------------------------------- /bin/lint_pod: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | echo "--- :rubygems: Setting up Gems" 4 | install_gems 5 | 6 | echo "--- :rubygems: Checking Gemfile.lock" 7 | validate_gemfile_lock 8 | 9 | echo "--- :rubocop: Running Rubocop" 10 | bundle exec rubocop 11 | 12 | if [ -f "Podfile.lock" ]; then 13 | echo "--- :cocoapods: Checking Podfile.lock" 14 | validate_podfile_lock 15 | fi 16 | 17 | # TODO: Add swiftlint -------------------------------------------------------------------------------- /bin/merge_junit_reports: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Initialize variables 4 | reports_dir="" 5 | output_file="" 6 | 7 | # Function to show usage 8 | usage() { 9 | echo "Usage: $0 -d -o " 10 | exit 1 11 | } 12 | 13 | # Parse command-line options 14 | while getopts "d:o:" opt; do 15 | case $opt in 16 | d) reports_dir=$OPTARG ;; 17 | o) output_file=$OPTARG ;; 18 | ?) usage ;; 19 | esac 20 | done 21 | 22 | # Check if both arguments were provided 23 | if [ -z "$reports_dir" ] || [ -z "$output_file" ]; then 24 | usage 25 | fi 26 | 27 | touch "$output_file" 28 | { 29 | echo '' 30 | # Write XML header to the output file 31 | echo '' 32 | 33 | # Merge the content of all input JUnit files in the directory. 34 | # Ignore tag in reports that are being merged, 35 | # as this tag should be a top-level, root tag for any report 36 | sed '/<\?xml .*\?>/d;//d; /<\/testsuites>/d' "$reports_dir"/*.xml 37 | 38 | # Close the testsuites tag 39 | echo '' 40 | } >> "$output_file" 41 | 42 | # Print the result 43 | echo "Merged XML reports into $output_file" 44 | -------------------------------------------------------------------------------- /bin/nvm_install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo 'The nvm_install utility has been removed and replaced by our nvm Buildkite plugin.' 4 | echo 'Please see this migration guide for details: https://github.com/Automattic/a8c-ci-toolkit-buildkite-plugin/blob/trunk/MIGRATION.md#from-200-to-300' 5 | 6 | exit 1 7 | -------------------------------------------------------------------------------- /bin/patch-cocoapods: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is a hack to workaround https://github.com/CocoaPods/CocoaPods/issues/12033. 4 | # We can remove this script once the issue is fixed. 5 | # 6 | # This script updates the cocoapods source code to change Xcode project targets' 7 | # minimal deployment target to 13.0, which is a requirement in newer Xcode versions. 8 | 9 | set -euo pipefail 10 | 11 | patch_content=$(cat <<'EOF' 12 | --- lib/cocoapods/installer/analyzer.rb 13 | +++ lib/cocoapods/installer/analyzer.rb 14 | @@ -857,7 +857,7 @@ 15 | Version.new(library_spec.deployment_target(platform_name) || default) 16 | end.max 17 | if platform_name == :ios && build_type.framework? 18 | - minimum = Version.new('8.0') 19 | + minimum = Version.new('13.0') 20 | deployment_target = [deployment_target, minimum].max 21 | end 22 | Platform.new(platform_name, deployment_target) 23 | --- lib/cocoapods/validator.rb 24 | +++ lib/cocoapods/validator.rb 25 | @@ -566,7 +566,7 @@ 26 | def deployment_target 27 | deployment_target = spec.subspec_by_name(subspec_name).deployment_target(consumer.platform_name) 28 | if consumer.platform_name == :ios && use_frameworks 29 | - minimum = Version.new('8.0') 30 | + minimum = Version.new('13.0') 31 | deployment_target = [Version.new(deployment_target), minimum].max.to_s 32 | end 33 | deployment_target 34 | EOF 35 | ) 36 | 37 | cocoapods_gem_path=$(bundle info cocoapods --path) 38 | echo "Path to the cocoapods gem: $cocoapods_gem_path" 39 | 40 | if echo "$patch_content" | patch --forward --force --directory "$cocoapods_gem_path" --strip 0; then 41 | echo "cocoapods patched successfully" 42 | exit 0 43 | fi 44 | 45 | echo "Failed to patch cocoapods. Try to re-apply the patch" 46 | echo "$patch_content" | patch --reverse --force --directory "$cocoapods_gem_path" --strip 0 47 | echo "$patch_content" | patch --forward --force --directory "$cocoapods_gem_path" --strip 0 48 | -------------------------------------------------------------------------------- /bin/path_aware_refreshenv.ps1: -------------------------------------------------------------------------------- 1 | # Wraps Chocolatey's `refreshenv` / `Update-SessionEnvironment` to avoid erasing PATH modifications. 2 | # 3 | # See https://docs.chocolatey.org/en-us/create/cmdlets/update-sessionenvironment/ 4 | # 5 | # Use this after installing a package via Chocolatey in a pipeline that modified the PATH at runtime, e.g. after adding a new binary to the PATH. 6 | # 7 | # It seems like calling refreshenv can erase PATH modifications that previous 8 | # steps in an automation script might have made. 9 | # 10 | # See for example the logs in 11 | # https://buildkite.com/automattic/beeper-desktop/builds/2893#01919717-d0d0-441d-a85d-0fe3223467d2/195 12 | # 13 | # To avoid the issue, we save the PATH pre-refreshenv and then manually add all 14 | # the components that were removed. 15 | 16 | # Stop script execution when a non-terminating error occurs 17 | $ErrorActionPreference = "Stop" 18 | 19 | Write-Output "PATH before refreshenv is $env:PATH" 20 | $originalPath = "$env:PATH" 21 | Write-Output "Calling refreshenv..." 22 | refreshenv 23 | $mergedPath = "$env:PATH;$originalPath" -split ";" | Select-Object -Unique -Skip 1 24 | $env:PATH = ($mergedPath -join ";") 25 | Write-Output "PATH after refreshenv is $env:PATH" 26 | -------------------------------------------------------------------------------- /bin/pr_changed_files: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # This script checks if files are changed in a PR. 4 | # 5 | # Usage: 6 | # pr_changed_files # Check if any files changed 7 | # pr_changed_files --all-match # Check if changes are limited to given patterns 8 | # pr_changed_files --any-match # Check if changes include files matching patterns 9 | # pr_changed_files [--stdout] ... # Add --stdout to output "true"/"false" to stdout instead of using exit code 10 | # 11 | # Result 12 | # - By default, the script exits with code 0 if the condition is met (files changed / patterns matched), 1 if not. This is typically ideal to use in `if` conditions. 13 | # - If you use the `--stdout` flag, it will output "true" or "false" instead of using the exit code (and exit 0 in both cases). Useful to assign the result to an environment variable for example. 14 | # - The script will exit with code 255 if there is an error in the command invocation (bad parameters, not in PR context, etc.) 15 | # 16 | # Typical usage patterns: 17 | # # Using exit codes 18 | # $ if pr_changed_files --any-match docs/* *.md; then 19 | # echo "Documentation was updated" 20 | # fi 21 | # 22 | # # Using stdout output 23 | # $ DOCS_UPDATED=$(pr_changed_files --stdout --any-match docs/* *.md) 24 | # 25 | # Behavior: 26 | # With no arguments: 27 | # Returns exit code 0 (or outputs "true" with `--stdout`) if the PR contains any changes 28 | # Returns exit code 1 (or outputs "false" with `--stdout`) if no changes 29 | # 30 | # With --all-match: 31 | # Returns exit code 0 (or outputs "true" with `--stdout`) if ALL changed files match AT LEAST ONE of the patterns 32 | # Returns exit code 1 (or outputs "false" with `--stdout`) if ANY changed file doesn't match ANY pattern 33 | # Note: Will report success even if not all patterns are matched by the changed files. 34 | # This mode is especially useful to check if the PR _only_ touches a particular subset of files/folders (but nothing else) 35 | # 36 | # With --any-match: 37 | # Returns exit code 0 (or outputs "true" with `--stdout`) if ANY changed file matches ANY of the patterns 38 | # Returns exit code 1 (or outputs "false" with `--stdout`) if NONE of the changed files match ANY pattern 39 | # Note: Will report success even if the PR includes other files not matching the patterns. 40 | # This mode is especially useful to check if the PR _includes_ (aka _contains at least_) particular files/folders 41 | # 42 | # Examples with expected outputs: 43 | # # Check if any files changed 44 | # $ pr_changed_files 45 | # → exit code 0 if PR has changes 46 | # → exit code 1 if PR has no changes 47 | # 48 | # # Check if only documentation files changed (to skip UI tests for example) 49 | # $ pr_changed_files --all-match "*.md" "docs/*" 50 | # → exit code 0 if PR only changes `docs/guide.md` and `README.md` 51 | # → exit code 0 if PR only changes `docs/image.png` (not all patterns need to match, ok if no *.md) 52 | # → exit code 1 if PR changes `docs/guide.md` and `src/main.swift` (ALL files need to match at least one pattern) 53 | # 54 | # # Check if any Swift files changed (to decide if we should run SwiftLint) 55 | # $ pr_changed_files --any-match "*.swift" ".swiftlint.yml" 56 | # → exit code 0 if PR changes `src/main.swift` and `README.md` (AT LEAST one file matches one of the patterns) 57 | # → exit code 0 if PR changes `.swiftlint.yml` 58 | # → exit code 1 if PR only changes `README.md` (none of files match any of the patterns) 59 | 60 | if [[ ! "${BUILDKITE_PULL_REQUEST:-invalid}" =~ ^[0-9]+$ ]]; then 61 | echo "Error: this tool can only be called from a Buildkite PR job" >&2 62 | exit 255 63 | fi 64 | 65 | # Ensure we have the base branch locally 66 | git fetch origin "$BUILDKITE_PULL_REQUEST_BASE_BRANCH" >/dev/null 2>&1 || { 67 | echo "Error: failed to fetch base branch '$BUILDKITE_PULL_REQUEST_BASE_BRANCH'" >&2 68 | exit 255 69 | } 70 | 71 | mode="" 72 | patterns=() 73 | use_stdout="false" 74 | 75 | # Define error message for mutually exclusive options 76 | EXCLUSIVE_OPTIONS_ERROR="Error: either specify --all-match or --any-match; cannot specify both" 77 | 78 | while [[ "$#" -gt 0 ]]; do 79 | case $1 in 80 | --stdout) 81 | use_stdout="true" 82 | shift 83 | ;; 84 | --all-match | --any-match) 85 | if [[ -n "$mode" ]]; then 86 | echo "$EXCLUSIVE_OPTIONS_ERROR" >&2 87 | exit 255 88 | fi 89 | mode="${1#--}" 90 | shift 91 | # Check if there are any patterns after the flag 92 | while [[ "$#" -gt 0 && "$1" != "--"* ]]; do 93 | patterns+=("$1") 94 | shift 95 | done 96 | if [[ "${#patterns[@]}" -eq 0 ]]; then 97 | echo "Error: must specify at least one file pattern" >&2 98 | exit 255 99 | fi 100 | ;; 101 | --*) 102 | echo "Error: unknown option $1" >&2 103 | exit 255 104 | ;; 105 | *) 106 | echo "Error: unexpected argument $1" >&2 107 | exit 255 108 | ;; 109 | esac 110 | done 111 | 112 | declare -a changed_files 113 | changed_files=() 114 | 115 | # Get list of changed files as an array 116 | while IFS= read -r -d '' file; do 117 | changed_files+=("$file") 118 | done < <(git --no-pager diff --name-only -z --merge-base origin/"$BUILDKITE_PULL_REQUEST_BASE_BRANCH" HEAD | sort) 119 | 120 | exit_true() { 121 | if [[ "$use_stdout" == "true" ]]; then 122 | echo "true" 123 | exit 0 124 | fi 125 | exit 0 126 | } 127 | 128 | exit_false() { 129 | if [[ "$use_stdout" == "true" ]]; then 130 | echo "false" 131 | exit 0 132 | fi 133 | exit 1 134 | } 135 | 136 | if [[ -z "$mode" ]]; then 137 | # No arguments = any change 138 | if [[ ${#changed_files[@]} -gt 0 ]]; then 139 | exit_true 140 | else 141 | exit_false 142 | fi 143 | fi 144 | 145 | # No changed files 146 | if [[ ${#changed_files[@]} -eq 0 ]]; then 147 | exit_false 148 | fi 149 | 150 | # Returns 0 if the file matches any of the patterns, 1 otherwise 151 | file_matches_any_pattern() { 152 | local file="$1" 153 | shift 154 | for pattern in "$@"; do 155 | # shellcheck disable=SC2053 # We don't quote the rhs in the condition on the next line because we want to interpret pattern as a glob pattern 156 | if [[ "$file" == ${pattern} ]]; then 157 | return 0 158 | fi 159 | done 160 | return 1 161 | } 162 | 163 | if [[ "$mode" == "all-match" ]]; then 164 | # Check if all changed files match at least one pattern 165 | for file in "${changed_files[@]}"; do 166 | if ! file_matches_any_pattern "$file" "${patterns[@]}"; then 167 | exit_false 168 | fi 169 | done 170 | exit_true 171 | elif [[ "$mode" == "any-match" ]]; then 172 | # Check if any changed file matches any pattern 173 | for file in "${changed_files[@]}"; do 174 | if file_matches_any_pattern "$file" "${patterns[@]}"; then 175 | exit_true 176 | fi 177 | done 178 | exit_false 179 | fi 180 | -------------------------------------------------------------------------------- /bin/prepare_to_publish_to_s3_params: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | PARAMS=(--sha1="$BUILDKITE_COMMIT") 4 | [[ -n "$BUILDKITE_TAG" ]] && PARAMS+=(--tag-name="$BUILDKITE_TAG") 5 | [[ -n "$BUILDKITE_BRANCH" ]] && PARAMS+=(--branch-name="$BUILDKITE_BRANCH") 6 | [[ -n "$BUILDKITE_PULL_REQUEST" && "$BUILDKITE_PULL_REQUEST" != "false" ]] && PARAMS+=(--pull-request-number="$BUILDKITE_PULL_REQUEST") 7 | echo "${PARAMS[@]}" 8 | -------------------------------------------------------------------------------- /bin/prepare_windows_host_for_app_distribution.ps1: -------------------------------------------------------------------------------- 1 | # Prepares a `windows` CI agent with all the necessary setup so it can build and distribute a windows app 2 | # 3 | # - Enables long path behavior 4 | # - Disable Windows Defender on the CI agent 5 | # - Install the "Chocolatey" package manager 6 | # - Enable dev mode so the agent can support Linux-style symlinks 7 | # - Download Code Signing Certificates(1) 8 | # - Install the Windows 10 SDK if it detected a `.windows-10-sdk-version` file(2) 9 | # 10 | # (1) The certificate it installs is stored in our AWS SecretsManager storage (`windows-code-signing-certificate` secret ID) 11 | # (2) You can skip the Windows 10 SDK installation regardless of whether `.windows-10-sdk-version` is present by calling the script with `-SkipWindows10SDKInstallation`. 12 | # 13 | # Note: In addition to calling this script, and depending on your client app, you might want to also install `npm` and the `Node.js` packages used by your client app on the agent too. For that part, you should use the `automattic/nvm` Buildkite plugin on the pipeline step's `plugins:` attribute. 14 | # 15 | 16 | param ( 17 | [switch]$SkipWindows10SDKInstallation = $false 18 | ) 19 | 20 | # Stop script execution when a non-terminating error occurs 21 | $ErrorActionPreference = "Stop" 22 | 23 | Write-Output "--- :windows: Setting up Windows for app distribution" 24 | 25 | Write-Output "Current working directory: $PWD" 26 | 27 | Write-Output "Enable long path behavior" 28 | # See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file#maximum-path-length-limitation 29 | Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem' -Name 'LongPathsEnabled' -Value 1 30 | 31 | # Disable Windows Defender before starting – otherwise our performance is terrible 32 | Write-Output "Disable Windows Defender..." 33 | $avPreference = @( 34 | @{DisableArchiveScanning = $true} 35 | @{DisableAutoExclusions = $true} 36 | @{DisableBehaviorMonitoring = $true} 37 | @{DisableBlockAtFirstSeen = $true} 38 | @{DisableCatchupFullScan = $true} 39 | @{DisableCatchupQuickScan = $true} 40 | @{DisableIntrusionPreventionSystem = $true} 41 | @{DisableIOAVProtection = $true} 42 | @{DisablePrivacyMode = $true} 43 | @{DisableScanningNetworkFiles = $true} 44 | @{DisableScriptScanning = $true} 45 | @{MAPSReporting = 0} 46 | @{PUAProtection = 0} 47 | @{SignatureDisableUpdateOnStartupWithoutEngine = $true} 48 | @{SubmitSamplesConsent = 2} 49 | @{ScanAvgCPULoadFactor = 5; ExclusionPath = @("D:\", "C:\")} 50 | @{DisableRealtimeMonitoring = $true} 51 | @{ScanScheduleDay = 8} 52 | ) 53 | 54 | $avPreference += @( 55 | @{EnableControlledFolderAccess = "Disable"} 56 | @{EnableNetworkProtection = "Disabled"} 57 | ) 58 | 59 | $avPreference | Foreach-Object { 60 | $avParams = $_ 61 | Set-MpPreference @avParams 62 | } 63 | 64 | # https://github.com/actions/runner-images/issues/4277 65 | # https://docs.microsoft.com/en-us/microsoft-365/security/defender-endpoint/microsoft-defender-antivirus-compatibility?view=o365-worldwide 66 | $atpRegPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows Advanced Threat Protection' 67 | if (Test-Path $atpRegPath) { 68 | Write-Output "Set Microsoft Defender Antivirus to passive mode" 69 | Set-ItemProperty -Path $atpRegPath -Name 'ForceDefenderPassiveMode' -Value '1' -Type 'DWORD' 70 | } 71 | 72 | # From https://stackoverflow.com/a/46760714 73 | Write-Output "--- :windows: Setting up Package Manager" 74 | $env:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.." 75 | Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" 76 | 77 | # This should avoid issues with symlinks not being supported in Windows. 78 | # 79 | # See how this build failed 80 | # https://buildkite.com/automattic/beeper-desktop/builds/2895#01919738-7c6e-4b82-8d1d-1c1800481740 81 | Write-Output "--- :windows: :linux: Enable developer mode to use symlinks" 82 | 83 | $developerMode = Get-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux 84 | 85 | if ($developerMode.State -eq 'Enabled') { 86 | Write-Output "Developer Mode is already enabled." 87 | } else { 88 | Write-Output "Enabling Developer Mode..." 89 | try { 90 | Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux -NoRestart 91 | } catch { 92 | Write-Output "Failed to enable Developer Mode. Continuing without it..." 93 | } 94 | } 95 | 96 | Write-Output "--- :lock_with_ink_pen: Download Code Signing Certificate" 97 | $certificateBinPath = "certificate.bin" 98 | $EncodedText = aws secretsmanager get-secret-value --secret-id windows-code-signing-certificate ` 99 | | jq -r '.SecretString' ` 100 | | Out-File $certificateBinPath 101 | $certificatePfxPath = "certificate.pfx" 102 | certutil -decode $certificateBinPath $certificatePfxPath 103 | Write-Output "Code signing certificate downloaded at: $((Get-Item $certificatePfxPath).FullName)" 104 | 105 | Write-Output "--- :windows: Checking whether to install Windows 10 SDK..." 106 | 107 | # When using Electron Forge and electron2appx, building Appx requires the Windows 10 SDK 108 | # 109 | # See https://github.com/hermit99/electron-windows-store/tree/v2.1.2?tab=readme-ov-file#usage 110 | 111 | if ($SkipWindows10SDKInstallation) { 112 | Write-Output "Run with SkipWindows10SDKInstallation = true. Skipping Windows 10 SDK installation check." 113 | exit 0 114 | } 115 | 116 | $windowsSDKVersionFile = ".windows-10-sdk-version" 117 | if (Test-Path $windowsSDKVersionFile) { 118 | Write-Output "Found $windowsSDKVersionFile file, installing Windows 10 SDK..." 119 | & "$PSScriptRoot\install_windows_10_sdk.ps1" 120 | If ($LastExitCode -ne 0) { Exit $LastExitCode } 121 | } else { 122 | Write-Output "No $windowsSDKVersionFile file found, skipping Windows 10 SDK installation." 123 | } 124 | -------------------------------------------------------------------------------- /bin/publish_pod: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Usage: publish_pod [OPTIONS] PODSPEC_PATH 4 | # 5 | # OPTIONS: 6 | # `--patch-cocoapods`: 7 | # Apply a patch to work around issues with older deployment targets — see https://github.com/CocoaPods/CocoaPods/issues/12033 8 | # `--allow-warnings`, `--synchronous`: 9 | # Those options are passed to `pod trunk push` verbatim. 10 | # 11 | # Note: Use `--synchronous` if you have co-dependant podspecs in your repo and need to publish multiple pods at the same time. 12 | # Without this option, since the first pod you push will take time to propagate thru the CocoaPods CDNs, attempting to push 13 | # the other dependant pod(s) in your repo might fail to find the first pushed pod until it has propagated thru CDNs. 14 | # 15 | 16 | PATCH_COCOAPODS="false" 17 | COCOAPODS_FLAGS=(--verbose) 18 | 19 | while [[ "$#" -gt 0 ]]; do 20 | case $1 in 21 | --patch-cocoapods) 22 | PATCH_COCOAPODS="true" 23 | ;; 24 | --allow-warnings | --synchronous) 25 | COCOAPODS_FLAGS+=("$1") 26 | ;; 27 | *) break ;; 28 | esac 29 | shift 30 | done 31 | 32 | PODSPEC_PATH=$1 33 | 34 | # POD_NAME=$(bundle exec pod ipc spec "$PODSPEC_PATH" | jq -r '.name') 35 | POD_VERSION=$(bundle exec pod ipc spec "$PODSPEC_PATH" | jq -r '.version') 36 | 37 | if [ -n "$BUILDKITE_TAG" ] && [ "$BUILDKITE_TAG" != "$POD_VERSION" ]; then 38 | echo "Tag $BUILDKITE_TAG does not match version $POD_VERSION from $PODSPEC_PATH." 39 | exit 1 40 | fi 41 | 42 | if [[ "${PATCH_COCOAPODS}" == 'true' ]]; then 43 | echo "⚠️ Remove this step once this issue is fixed: https://github.com/CocoaPods/CocoaPods/issues/12033" 44 | patch-cocoapods 45 | fi 46 | 47 | # For some reason this fixes a failure in `lib lint` 48 | # https://github.com/Automattic/buildkite-ci/issues/7 49 | xcrun simctl list >> /dev/null 50 | 51 | bundle exec pod trunk push "${COCOAPODS_FLAGS[@]}" "$PODSPEC_PATH" 52 | -------------------------------------------------------------------------------- /bin/publish_private_pod: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Usage: publish_private_pod [OPTIONS] PODSPEC_PATH PRIVATE_SPECS_REPO DEPLOY_KEY 4 | # 5 | # OPTIONS: 6 | # `--patch-cocoapods`: 7 | # Apply a patch to work around issues with older deployment targets — see https://github.com/CocoaPods/CocoaPods/issues/12033 8 | # `--allow-warnings`: 9 | # Allow warnings in `pod repo push`. 10 | 11 | PATCH_COCOAPODS="false" 12 | COCOAPODS_FLAGS=(--verbose) 13 | 14 | while [[ "$#" -gt 0 ]]; do 15 | case $1 in 16 | --patch-cocoapods) 17 | PATCH_COCOAPODS="true" 18 | ;; 19 | --allow-warnings) 20 | COCOAPODS_FLAGS+=("$1") 21 | ;; 22 | *) break ;; 23 | esac 24 | shift 25 | done 26 | 27 | PODSPEC_PATH=$1 28 | PRIVATE_SPECS_REPO=$2 29 | DEPLOY_KEY=$3 30 | 31 | echo "$DEPLOY_KEY" > ~/.ssh/pod_repo_push_deploy_key 32 | chmod 0600 ~/.ssh/pod_repo_push_deploy_key 33 | 34 | # Remove all existing keys, and replace them with the deploy key 35 | ssh-add -D 36 | ssh-add ~/.ssh/pod_repo_push_deploy_key 37 | ssh-add -l 38 | 39 | # Add the host of the spec repo (typically github.com) to the known_hosts to be sure we can clone it via ssh 40 | add_host_to_ssh_known_hosts "$PRIVATE_SPECS_REPO" 41 | 42 | if [[ "${PATCH_COCOAPODS}" == 'true' ]]; then 43 | echo "⚠️ Remove this step once this issue is fixed: https://github.com/CocoaPods/CocoaPods/issues/12033" 44 | patch-cocoapods 45 | fi 46 | 47 | # For some reason this fixes a failure in `lib lint` 48 | # https://github.com/Automattic/buildkite-ci/issues/7 49 | xcrun simctl list >> /dev/null 50 | 51 | bundle exec pod repo push "${COCOAPODS_FLAGS[@]}" "$PRIVATE_SPECS_REPO" "$PODSPEC_PATH" 52 | -------------------------------------------------------------------------------- /bin/restore_cache: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | CACHE_KEY=$1 4 | 5 | bytes_to_mb() { 6 | local bytes=$1 7 | printf "%.2f" "$(awk "BEGIN {print $bytes / (1024 * 1024)}")" 8 | } 9 | 10 | S3_BUCKET_NAME=${CACHE_BUCKET_NAME-} 11 | if [ -z "$S3_BUCKET_NAME" ]; then 12 | if [ -z "$BUILDKITE_PLUGIN_A8C_CI_TOOLKIT_BUCKET" ]; then 13 | echo "⛔Unable to restore file from cache – no \$CACHE_BUCKET_NAME is set" 14 | exit 1 15 | else 16 | echo "Reading bucket name from 'BUILDKITE_PLUGIN_A8C_CI_TOOLKIT_BUCKET'" 17 | S3_BUCKET_NAME="$BUILDKITE_PLUGIN_A8C_CI_TOOLKIT_BUCKET" 18 | fi 19 | fi 20 | 21 | echo "Using $S3_BUCKET_NAME as cache bucket" 22 | 23 | if aws s3api head-object --bucket "$S3_BUCKET_NAME" --key "$CACHE_KEY" > /dev/null 2>&1; then 24 | SECONDS=0 25 | echo "Restoring cache entry $CACHE_KEY" 26 | 27 | echo " Downloading" 28 | # If the bucket has transfer acceleration enabled, use it! 29 | ACCELERATION_STATUS=$(aws s3api get-bucket-accelerate-configuration --bucket "$S3_BUCKET_NAME" | jq '.Status' -r || true) 30 | 31 | if [ "$ACCELERATION_STATUS" = "Enabled" ]; then 32 | echo "Downloading with transfer acceleration" 33 | aws s3 cp "s3://$S3_BUCKET_NAME/$CACHE_KEY" "$CACHE_KEY" --quiet --endpoint-url https://s3-accelerate.amazonaws.com 34 | else 35 | aws s3 cp "s3://$S3_BUCKET_NAME/$CACHE_KEY" "$CACHE_KEY" --quiet 36 | fi 37 | 38 | CACHE_SIZE=$(wc -c < "$CACHE_KEY") 39 | echo " Decompressing" 40 | tar -xf "$CACHE_KEY" 41 | 42 | echo " Cleaning Up" 43 | rm "$CACHE_KEY" 44 | 45 | duration=$SECONDS 46 | echo "Cache entry successfully restored" 47 | echo " Duration: $((duration / 60)) minutes and $((duration % 60)) seconds" 48 | echo " Cache Size: $(bytes_to_mb "$CACHE_SIZE") MB" 49 | else 50 | echo "No cache entry found for '$CACHE_KEY'" 51 | fi 52 | -------------------------------------------------------------------------------- /bin/restore_gradle_dependency_cache: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # The key is shared with `bin/save_gradle_dependency_cache` 4 | GRADLE_DEPENDENCY_CACHE_KEY="${BUILDKITE_PIPELINE_SLUG}_GRADLE_DEPENDENCY_CACHE_V2" 5 | 6 | echo "Restoring Gradle dependency cache..." 7 | 8 | # The directory is shared with `bin/save_gradle_dependency_cache` 9 | GRADLE_DEP_CACHE="$GRADLE_HOME/dependency-cache" 10 | 11 | DEP_CACHE_BASE_FOLDER=$(dirname "$GRADLE_DEP_CACHE") 12 | DEP_CACHE_FOLDER_NAME=$(basename "$GRADLE_DEP_CACHE") 13 | 14 | # `save_cache` & `restore_cache` scripts only work if they are called from the same directory 15 | pushd "$DEP_CACHE_BASE_FOLDER" 16 | restore_cache "$GRADLE_DEPENDENCY_CACHE_KEY" 17 | 18 | if [ -d "$DEP_CACHE_FOLDER_NAME/modules-2/" ]; then 19 | echo "Placing Gradle dependency cache..." 20 | mv "$DEP_CACHE_FOLDER_NAME/modules-2/"* caches/modules-2/ 21 | rm -r "$DEP_CACHE_FOLDER_NAME" 22 | fi 23 | 24 | popd 25 | -------------------------------------------------------------------------------- /bin/run_swiftlint: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | echo "--- :swift: Running SwiftLint" 4 | 5 | SWIFTLINT_ARGUMENTS=(--quiet --reporter relative-path) 6 | 7 | while [[ "$#" -gt 0 ]]; do 8 | case $1 in 9 | --strict | --lenient) 10 | SWIFTLINT_ARGUMENTS+=("$1") 11 | shift 12 | ;; 13 | *) break ;; 14 | esac 15 | done 16 | 17 | if [[ $# -gt 0 ]]; then 18 | echo "Error: invalid arguments." 19 | echo "Usage: $0 [--strict | --lenient]" 20 | exit 1 21 | fi 22 | 23 | SWIFTLINT_VERSION="$(<.swiftlint.yml awk '/^swiftlint_version: / {print $2}')" 24 | if [ -z "$SWIFTLINT_VERSION" ]; then 25 | echo "Your \`.swiftlint.yml\` file must contain a key for \`swiftlint_version:\` so that we know which version of SwiftLint to use to lint your codebase." 26 | exit 1 27 | fi 28 | SWIFTLINT_DOCKER_CMD=(docker run --rm -v "$PWD":/workspace -w /workspace ghcr.io/realm/swiftlint:"$SWIFTLINT_VERSION" swiftlint) 29 | 30 | set +e 31 | SWIFTLINT_OUTPUT=$("${SWIFTLINT_DOCKER_CMD[@]}" lint "${SWIFTLINT_ARGUMENTS[@]}") 32 | SWIFTLINT_EXIT_STATUS=$? 33 | set -e 34 | 35 | WARNINGS=$(echo -e "$SWIFTLINT_OUTPUT" | awk -F': ' '/: warning:/ {printf "- `%s`: %s\n", $1, $4}') 36 | ERRORS=$(echo -e "$SWIFTLINT_OUTPUT" | awk -F': ' '/: error:/ {printf "- `%s`: %s\n", $1, $4}') 37 | 38 | if [ -n "$WARNINGS" ]; then 39 | echo "$WARNINGS" 40 | printf "**SwiftLint Warnings**\n%b" "$WARNINGS" | buildkite-agent annotate --style 'warning' 41 | fi 42 | 43 | if [ -n "$ERRORS" ]; then 44 | echo "$ERRORS" 45 | printf "**SwiftLint Errors**\n%b" "$ERRORS" | buildkite-agent annotate --style 'error' 46 | fi 47 | 48 | exit $SWIFTLINT_EXIT_STATUS 49 | -------------------------------------------------------------------------------- /bin/save_cache: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | CACHE_FILE=$1 4 | CACHE_KEY=$2 5 | 6 | bytes_to_mb() { 7 | local bytes=$1 8 | printf "%.2f" "$(awk "BEGIN {print $bytes / (1024 * 1024)}")" 9 | } 10 | 11 | if [ -z "$CACHE_FILE" ]; then 12 | echo "You must pass the file or directory you want to be cached" 13 | exit 1 14 | fi 15 | 16 | # We can automatically derive a cache key if one isn't provided 17 | if [ -z "$CACHE_KEY" ]; then 18 | echo "No cache key provided – automatically deriving one:" 19 | 20 | # if the $CACHE_FILE is a directory, derived the key from the hash of all files within it 21 | if [[ -d $CACHE_FILE ]]; then 22 | CACHE_KEY=$(hash_directory "$CACHE_FILE") 23 | echo " '$CACHE_FILE' is a directory with the hash $CACHE_KEY" 24 | 25 | # if the $CACHE_FILE is a regular file, derive the key from the file's hash 26 | elif [[ -f $CACHE_FILE ]]; then 27 | CACHE_KEY=$(hash_file "$CACHE_FILE") 28 | echo " '$CACHE_FILE' is a file with the hash $CACHE_KEY" 29 | fi 30 | fi 31 | 32 | S3_BUCKET_NAME=${CACHE_BUCKET_NAME-} 33 | if [ -z "$S3_BUCKET_NAME" ]; then 34 | if [ -z "$BUILDKITE_PLUGIN_A8C_CI_TOOLKIT_BUCKET" ]; then 35 | echo "⛔Unable to save file to cache – no \$CACHE_BUCKET_NAME is set" 36 | exit 1 37 | else 38 | echo "Reading bucket name from 'BUILDKITE_PLUGIN_A8C_CI_TOOLKIT_BUCKET'" 39 | S3_BUCKET_NAME="$BUILDKITE_PLUGIN_A8C_CI_TOOLKIT_BUCKET" 40 | fi 41 | fi 42 | 43 | echo "Using $S3_BUCKET_NAME as cache bucket" 44 | 45 | # Use with caution – in general it's not a good idea to overwrite a cache entry 46 | SHOULD_FORCE=${3-false} 47 | if [[ "$SHOULD_FORCE" == '--force' ]]; then 48 | echo "Deleting the existing cache key" 49 | aws s3 rm "s3://$S3_BUCKET_NAME/$CACHE_KEY" 50 | fi 51 | 52 | if ! aws s3api head-object --bucket "$S3_BUCKET_NAME" --key "$CACHE_KEY" > /dev/null 2>&1; then 53 | SECONDS=0 54 | echo "No existing cache entry for $CACHE_KEY – storing in cache" 55 | 56 | echo " Compressing" 57 | TAR_CONFIG=${4-} 58 | if [[ "$TAR_CONFIG" == '--use_relative_path_in_tar' ]]; then 59 | # This is used by actions such as `install_swiftpm_dependencies` 60 | # This configuration allows the tar to not include the full system path of the 61 | # directory that's being archived. For example, this will save only the 62 | # "DIRECTORY_BEING_ARCHIVED" in `/User/builder/DIRECTORY_BEING_ARCHIVED` 63 | # instead of also creating `/User/builder` when extracting the archive 64 | tar -czf "$CACHE_KEY" -C "$CACHE_FILE" . 65 | else 66 | tar -czf "$CACHE_KEY" "$CACHE_FILE" 67 | fi 68 | CACHE_SIZE=$(wc -c < "$CACHE_KEY") 69 | 70 | echo " Uploading" 71 | # If the bucket has transfer acceleration enabled, use it! 72 | ACCELERATION_STATUS=$(aws s3api get-bucket-accelerate-configuration --bucket "$S3_BUCKET_NAME" | jq '.Status' -r || true) 73 | 74 | if [ "$ACCELERATION_STATUS" = "Enabled" ]; then 75 | echo "Uploading with transfer acceleration" 76 | aws s3 cp "$CACHE_KEY" "s3://$S3_BUCKET_NAME/$CACHE_KEY" --quiet --endpoint-url https://s3-accelerate.amazonaws.com 77 | else 78 | aws s3 cp "$CACHE_KEY" "s3://$S3_BUCKET_NAME/$CACHE_KEY" --quiet 79 | fi 80 | 81 | echo " Cleaning Up" 82 | rm "$CACHE_KEY" 83 | 84 | duration=$SECONDS 85 | echo "Cache entry successfully saved" 86 | echo " Duration: $((duration / 60)) minutes and $((duration % 60)) seconds" 87 | echo " Cache Size: $(bytes_to_mb "$CACHE_SIZE") MB" 88 | else 89 | echo "This file is already cached – skipping upload" 90 | fi 91 | -------------------------------------------------------------------------------- /bin/save_gradle_dependency_cache: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # The key is shared with `bin/restore_gradle_dependency_cache` 4 | GRADLE_DEPENDENCY_CACHE_KEY="${BUILDKITE_PIPELINE_SLUG}_GRADLE_DEPENDENCY_CACHE_V2" 5 | 6 | echo "Saving Gradle dependency cache..." 7 | 8 | # The directory is shared with `bin/restore_gradle_dependency_cache` 9 | GRADLE_DEP_CACHE="$GRADLE_HOME/dependency-cache" 10 | 11 | mkdir -p "$GRADLE_DEP_CACHE" 12 | 13 | # https://docs.gradle.org/current/userguide/dependency_resolution.html#sub:cache_copy 14 | # Gradle suggests removing the `*.lock` files and the `gc.properties` file before saving cache 15 | cp -r ~/.gradle/caches/modules-2 "$GRADLE_DEP_CACHE" \ 16 | && find "$GRADLE_DEP_CACHE" -name "*.lock" -type f -delete \ 17 | && find "$GRADLE_DEP_CACHE" -name "gc.properties" -type f -delete 18 | 19 | DEP_CACHE_BASE_FOLDER=$(dirname "$GRADLE_DEP_CACHE") 20 | DEP_CACHE_FOLDER_NAME=$(basename "$GRADLE_DEP_CACHE") 21 | 22 | # `save_cache` & `restore_cache` scripts only work if they are called from the same directory 23 | pushd "$DEP_CACHE_BASE_FOLDER" 24 | # For now we are using a single key - we might expand on this later by using dependency catalog version 25 | save_cache "$DEP_CACHE_FOLDER_NAME" "$GRADLE_DEPENDENCY_CACHE_KEY" --force 26 | popd 27 | -------------------------------------------------------------------------------- /bin/slack_notify_pod_published: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | PODSPEC_PATH=$1 4 | SLACK_WEBHOOK=$2 5 | 6 | POD_NAME=$(bundle exec pod ipc spec "$PODSPEC_PATH" | jq -r '.name') 7 | POD_VERSION=$(bundle exec pod ipc spec "$PODSPEC_PATH" | jq -r '.version') 8 | 9 | MESSAGE=":tada: ${POD_NAME} ${POD_VERSION} successfully published to CocoaPods trunk!\nIt will take a few minutes for this version to be deployed to the CocoaPods CDN." 10 | curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"${MESSAGE}\"}" "$SLACK_WEBHOOK" 11 | -------------------------------------------------------------------------------- /bin/upload_artifact: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Usage 4 | # upload_artifact $file_path 5 | # 6 | # $file_path is the path to a file on disk. It'll automatically be combined with the current build ID to differentiate between 7 | # the same file in different jobs so that it can be correctly re-downloaded within the same job. 8 | 9 | if [ -z "${1-}" ]; then 10 | echo "You must pass the file you want to be stored" 11 | exit 1 12 | else 13 | ARTIFACT_PATH=$1 14 | fi 15 | 16 | if [ ! -f "$ARTIFACT_PATH" ]; then 17 | echo "No file found at $ARTIFACT_PATH" 18 | exit 2 19 | fi 20 | 21 | BUCKET=${ARTIFACTS_S3_BUCKET-} 22 | 23 | if [ -z "$BUCKET" ]; then 24 | echo "You must pass set the \`ARTIFACTS_S3_BUCKET\` environment variable with the S3 bucket you'd like to use" 25 | exit 3 26 | fi 27 | 28 | BASENAME=$(basename "$ARTIFACT_PATH") 29 | KEY="$BUILDKITE_BUILD_ID/$BASENAME" 30 | 31 | # If the bucket has transfer acceleration enabled, use it! 32 | ACCELERATION_STATUS=$(aws s3api get-bucket-accelerate-configuration --bucket "$BUCKET" | jq '.Status' -r || true) 33 | 34 | if [ "$ACCELERATION_STATUS" = "Enabled" ]; then 35 | echo "Uploading with transfer acceleration" 36 | aws s3 cp "$ARTIFACT_PATH" "s3://$BUCKET/$KEY" --endpoint-url https://s3-accelerate.amazonaws.com 37 | else 38 | aws s3 cp "$ARTIFACT_PATH" "s3://$BUCKET/$KEY" 39 | fi 40 | -------------------------------------------------------------------------------- /bin/upload_buildkite_test_analytics_junit: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | if [ -z "${1: }" ]; then 4 | echo "You must pass a path to the file you want to upload to Test Analytics" 5 | exit 1 6 | fi 7 | 8 | if [ -z "${2: }" ]; then 9 | echo "You must pass a Buildkite Analytics Token" 10 | exit 1 11 | fi 12 | 13 | FILE_PATH=$1 14 | ANALYTICS_TOKEN=$2 15 | 16 | if [ ! -f "$FILE_PATH" ]; then 17 | echo "No file exists at $FILE_PATH. Cancelling Upload" 18 | 19 | # Clean exit, to avoid invalidating an entire run because of a missing artifact 20 | exit 0 21 | fi 22 | 23 | echo "Uploading ${FILE_PATH} to Test Analytics" 24 | 25 | curl -X POST \ 26 | -H "Authorization: Token token=$ANALYTICS_TOKEN" \ 27 | -F "format=junit" \ 28 | -F "data=@$FILE_PATH" \ 29 | -F "run_env[CI]=buildkite" \ 30 | -F "run_env[key]=$BUILDKITE_BUILD_ID" \ 31 | -F "run_env[number]=$BUILDKITE_BUILD_NUMBER" \ 32 | -F "run_env[job_id]=$BUILDKITE_JOB_ID" \ 33 | -F "run_env[branch]=$BUILDKITE_BRANCH" \ 34 | -F "run_env[commit_sha]=$BUILDKITE_COMMIT" \ 35 | -F "run_env[message]=$BUILDKITE_MESSAGE" \ 36 | -F "run_env[url]=$BUILDKITE_BUILD_URL" \ 37 | https://analytics-api.buildkite.com/v1/uploads 38 | -------------------------------------------------------------------------------- /bin/upload_sarif_to_github: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | sarif_file="${1:-}" 6 | 7 | [[ $BUILDKITE_REPO =~ ^(https?://|git@)([^/:]+)[/:](.*)$ ]] && slug=${BASH_REMATCH[3]%%.git} 8 | if [[ -z "${slug:-}" ]]; then 9 | echo "Unable to determine the repo slug from the repo URL" 10 | exit 1 11 | fi 12 | 13 | if [ -z "$sarif_file" ]; then 14 | echo "Not enough arguments provided. Usage: ./upload_sarif_to_gh.sh " 15 | exit 1 16 | fi 17 | 18 | if [ ! -f "$sarif_file" ]; then 19 | echo "Error: The specified sarif file '$sarif_file' does not exist or is not a regular file." 20 | exit 1 21 | fi 22 | 23 | if [ ! -r "$sarif_file" ]; then 24 | echo "Error: The specified sarif file '$sarif_file' is not readable." 25 | exit 1 26 | fi 27 | 28 | # Check that GITHUB_TOKEN is set 29 | if [ -z "${GITHUB_TOKEN:-}" ]; then 30 | echo "Error: GITHUB_TOKEN is not defined." 31 | exit 1 32 | fi 33 | 34 | sarif_base64_temp_file=$(mktemp) 35 | 36 | gzip -c "$sarif_file" | base64 > "$sarif_base64_temp_file" 37 | 38 | if [[ "${BUILDKITE_PULL_REQUEST:-false}" != "false" ]]; then 39 | json=$(jq -n \ 40 | --arg commit_sha "$BUILDKITE_COMMIT" \ 41 | --arg pr_number "$BUILDKITE_PULL_REQUEST" \ 42 | --rawfile sarif "$sarif_base64_temp_file" \ 43 | '{ 44 | "commit_sha": $commit_sha, 45 | "ref": ("refs/pull/"+$pr_number+"/head"), 46 | "sarif": $sarif 47 | }') 48 | elif [[ "$BUILDKITE_BRANCH" == "$BUILDKITE_PIPELINE_DEFAULT_BRANCH" ]]; then 49 | json=$(jq -n \ 50 | --arg commit_sha "$BUILDKITE_COMMIT" \ 51 | --arg branch "$BUILDKITE_BRANCH" \ 52 | --rawfile sarif "$sarif_base64_temp_file" \ 53 | '{ 54 | "commit_sha": $commit_sha, 55 | "ref": ("refs/heads/"+$branch), 56 | "sarif": $sarif 57 | }') 58 | else 59 | exit 0 60 | fi 61 | 62 | sarif_json_temp_file=$(mktemp) 63 | echo "$json" > "$sarif_json_temp_file" 64 | 65 | github_api "repos/$slug/code-scanning/sarifs" --data-binary "@$sarif_json_temp_file" 66 | -------------------------------------------------------------------------------- /bin/validate_gemfile_lock: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # We always need to run `bundle install` first, otherwise we can't compare 4 | bundle install 5 | 6 | if git status | grep modified | grep -q Gemfile.lock; then 7 | echo "Error: Gemfile.lock is not in sync – please run \`bundle install\` and commit your changes" 8 | echo '' 9 | echo 'Gemfile.lock diff:' 10 | echo '' 11 | git diff Gemfile.lock 12 | exit 1 13 | fi 14 | 15 | echo "Gemfile.lock is in sync" 16 | -------------------------------------------------------------------------------- /bin/validate_gradle_wrapper: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | CHECKSUM_URLS=$(curl -s https://services.gradle.org/versions/all | jq -r ".[].wrapperChecksumUrl? | select(.)") 4 | 5 | function validate_checksum() { 6 | wrapper_file="$1" 7 | validated_with_checksum_from_url="" 8 | 9 | sha256_checksum_to_be_validated=$(hash_file "$wrapper_file") 10 | 11 | while IFS= read -r checksum_url; do 12 | downloaded_checksum=$(curl -s --location "$checksum_url") 13 | if [[ "$sha256_checksum_to_be_validated" == "$downloaded_checksum" ]]; then 14 | validated_with_checksum_from_url="$checksum_url" 15 | break; 16 | fi 17 | done <<< "$CHECKSUM_URLS" 18 | 19 | if [[ -z "$validated_with_checksum_from_url" ]]; then 20 | echo "Failed to validate '$wrapper_file'" 21 | exit 1 22 | else 23 | echo "'$wrapper_file' is validated with sha256 checksum from '$validated_with_checksum_from_url'" 24 | fi 25 | } 26 | 27 | WRAPPER_JARS=$(find . -type f -name "gradle-wrapper.jar") 28 | if [ -z "${WRAPPER_JARS}" ]; then 29 | echo "No gradle-wrapper.jar files found." 30 | exit 1 31 | else 32 | while IFS= read -r wrapper_file; do 33 | validate_checksum "$wrapper_file" 34 | done <<< "$WRAPPER_JARS" 35 | fi 36 | -------------------------------------------------------------------------------- /bin/validate_podfile_lock: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | PODFILE_SHA1=$(ruby -e "require 'yaml';puts YAML.load_file('Podfile.lock')['PODFILE CHECKSUM']") 4 | RESULT=$(echo "$PODFILE_SHA1 *Podfile" | shasum -c) 5 | 6 | if [[ $RESULT != "Podfile: OK" ]]; then 7 | echo "Error: Podfile.lock is not in sync – please run \`bundle exec pod install\` and commit your changes" 8 | exit 1 9 | fi 10 | 11 | echo "Podfile.lock is in sync" 12 | -------------------------------------------------------------------------------- /bin/validate_podspec: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | # Usage: validate_podspec [OPTIONS] [PODSPEC(S)_PATH(S)...] 4 | # 5 | # - If no `PODSPEC_PATH` provided, will lint all `*.podspec` files found 6 | # - By default, the linting of each podspec will `--include-podspecs=*.podspec` any other podspec 7 | # found in the root of the repo, in order to support repos containing co-dependant pods which 8 | # needs to be linted and pushed in concert 9 | # 10 | # OPTIONS: 11 | # `--patch-cocoapods`: 12 | # Apply a patch to work around issues with older deployment targets — see https://github.com/CocoaPods/CocoaPods/issues/12033 13 | # `--allow-warnings`, `--sources=…`, `--private`, `--include-podspecs=…`, `--external-podspecs=…`: 14 | # Those options are passed to `pod lib lint` verbatim 15 | # 16 | 17 | PATCH_COCOAPODS="false" 18 | COCOAPODS_FLAGS=(--verbose --fail-fast) 19 | 20 | while [[ "$#" -gt 0 ]]; do 21 | case $1 in 22 | --patch-cocoapods) 23 | PATCH_COCOAPODS="true" 24 | ;; 25 | --allow-warnings | --sources=* | --private | --include-podspecs=* | --external-podspecs=*) 26 | COCOAPODS_FLAGS+=("$1") 27 | ;; 28 | *) break ;; 29 | esac 30 | shift 31 | done 32 | 33 | if [[ ! "${COCOAPODS_FLAGS[*]}" =~ --include-podspecs=* ]]; then 34 | # By default, and if that flag was not already provided explicitly, include all other podspecs present 35 | # in the same repo as part of validation, so that if a repo contains multiple co-dependant podspecs, 36 | # validation will use the local podspecs of those co-dependant pods when validating each individual pod. 37 | # 38 | # Note: `pod lib lint` considers it invalid to provide that parameter multiple times, hence testing for it first. 39 | # Note: If a client needs to override this to make sure _not_ to include any other podspec for some special case 40 | # or reason, one can pass `--include-podspecs=""` 41 | COCOAPODS_FLAGS+=("--include-podspecs=*.podspec") 42 | fi 43 | 44 | echo "--- :rubygems: Setting up Gems" 45 | install_gems 46 | 47 | if [[ "${PATCH_COCOAPODS}" == 'true' ]]; then 48 | echo "--- :writing_hand: Patching cocoapods" 49 | echo "⚠️ Remove this step once this issue is fixed: https://github.com/CocoaPods/CocoaPods/issues/12033" 50 | patch-cocoapods 51 | fi 52 | 53 | if [ -f "Podfile.lock" ]; then 54 | echo "--- :cocoapods: Setting up Pods" 55 | install_cocoapods 56 | fi 57 | 58 | echo "--- :microscope: Validate Podspec" 59 | # For some reason this fixes a failure in `lib lint` 60 | # https://github.com/Automattic/buildkite-ci/issues/7 61 | xcrun simctl list >> /dev/null 62 | 63 | bundle exec pod lib lint "${COCOAPODS_FLAGS[@]}" -- "$@" 64 | -------------------------------------------------------------------------------- /bin/validate_swift_package: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | # 3 | # Any arguments passed to this script will be passed through to `fastlane`. 4 | # If no argument is passed, the `test` lane will be called by default. 5 | 6 | if [[ ! -f Package.swift ]]; then 7 | echo "This repo is not a Swift package." 8 | exit 1 9 | fi 10 | 11 | echo "--- :rubygems: Setting up Gems" 12 | install_gems 13 | 14 | echo "--- :test_tube: Building and testing the Swift Package" 15 | bundle exec fastlane "${@:-test}" 16 | -------------------------------------------------------------------------------- /hooks/environment: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # The `pre-command` hook will run just before your build command runs 4 | 5 | # Note that as the script is sourced not run directly, the shebang line will be ignored 6 | # See https://buildkite.com/docs/agent/v3/hooks#creating-hook-scripts 7 | 8 | set -e 9 | 10 | echo "~~~ :file_cabinet: Loading Automattic plugin commands in the environment" 11 | 12 | HOOKS_ROOT=$( dirname "${BASH_SOURCE[0]}" ) 13 | PLUGIN_ROOT=$( dirname "$HOOKS_ROOT" ) 14 | 15 | OS=$(uname -s) 16 | echo "FYI: Running on OS $OS" 17 | 18 | # Notice that we don't need any OS-specific treatment of the path format for UNIX vs Windows. 19 | # When Windows runs this script via bash, it handles the path separation conversion from / to \ internally. 20 | PLUGIN_BIN="${PLUGIN_ROOT}/bin" 21 | 22 | echo "Original PATH: $PATH" 23 | export PATH="$PATH:$PLUGIN_BIN" 24 | echo "PATH updated to: $PATH" 25 | -------------------------------------------------------------------------------- /jar/dependency-diff-tldr-0.0.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/a8c-ci-toolkit-buildkite-plugin/0d65ae9c130f1bcf02079dc9f7fb715890dad4cb/jar/dependency-diff-tldr-0.0.6.jar -------------------------------------------------------------------------------- /plugin.yml: -------------------------------------------------------------------------------- 1 | name: CI Toolkit 2 | description: A library of commonly used commands for your CI builds. 3 | author: https://github.com/automattic 4 | requirements: 5 | - aws 6 | - gh 7 | - jq 8 | - node 9 | - ruby 10 | - xcodebuild 11 | - xcrun 12 | configuration: 13 | properties: 14 | bucket: 15 | type: string 16 | additionalProperties: false 17 | -------------------------------------------------------------------------------- /tests/environment.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "$BATS_PLUGIN_PATH/load.bash" 4 | 5 | fail() { 6 | echo "Failure: $1" 7 | exit 1 8 | } 9 | 10 | @test "Exposes all commands in bin/" { 11 | source "$PWD/hooks/environment" 12 | for f in "bin"/*; do 13 | cmd=$(basename "$f") 14 | which "$cmd" > /dev/null || fail "$cmd is not exposed to \$PATH" 15 | done 16 | } 17 | 18 | # There are additional Ruby tests in this directory for things that are more easily tested in that context 19 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/Makefile: -------------------------------------------------------------------------------- 1 | regenerate_fixtures: 2 | xcodegen generate --spec ./project/project.yml 3 | xcodebuild -resolvePackageDependencies -project ./project/Demo.xcodeproj 4 | cp ./project/Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ./package_resolved_fixtures/valid.resolved 5 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/README.md: -------------------------------------------------------------------------------- 1 | # Integration tests for `install_swiftpm_dependencies` 2 | 3 | This folder contains a variety of Xcode and Swift Package projects to test the behavior of the `install_swiftpm_dependencies` in various configurations. 4 | 5 | Note that currently with "testing" we mean: making sure the CI build doesn't fail. There isn't yet an automated setup to verify the correct command behavior. 6 | 7 | Also note that the tests are intentionally at the integration / end-to-end level. We could structure `install_swiftpm_dependencies` so that it spits out the `xcodebuild` or `swift` it plans to run and test that, but we would lose important feedback on how `xcodebuild` and `swift` themselves behave. 8 | Feedback on Apple's tooling behavior is crucial to get, given their known quirks. 9 | 10 | ## Development 11 | 12 | The integration tests use [XcodeGen](https://github.com/yonaskolb/XcodeGen) to code-generate the project files and keep the repository footprint small. 13 | 14 | However, by code-generating the xcodeproj file we lose tracking of the `Package.resolved` Xcode generates at build time, which is particularly problematic in CI. 15 | To compensate for this, we have dedicated automation that tracks a reference `Package.resolved` file for CI to use. 16 | 17 | If resolved file format were to change in the future, you can update the reference file via `make regenerate_resolved_fixtures`. 18 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/package/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | xcuserdata/ 5 | DerivedData/ 6 | .swiftpm/configuration/registries.json 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | .netrc 9 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/package/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "c908e0d89a41be29a11cf4ce76b34002b6c4863ff5d7eafd623b36c267284b12", 3 | "pins" : [ 4 | { 5 | "identity" : "screenobject", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/Automattic/ScreenObject", 8 | "state" : { 9 | "revision" : "328db56c62aab91440ec5e07cc9f7eef6e26a26e", 10 | "version" : "0.2.3" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/package/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.10 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Demo", 7 | products: [ 8 | .library( 9 | name: "Demo", 10 | targets: ["Demo"]), 11 | ], 12 | dependencies: [ 13 | .package(url: "https://github.com/Automattic/ScreenObject", from: "0.2.3") 14 | ], 15 | targets: [ 16 | .target( 17 | name: "Demo"), 18 | .testTarget( 19 | name: "DemoTests", 20 | dependencies: ["Demo"]), 21 | ] 22 | ) 23 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/package/Sources/Demo/Demo.swift: -------------------------------------------------------------------------------- 1 | struct Demo { 2 | static func sayHello() -> String { 3 | "Hello, world!" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/package/Tests/DemoTests/DemoTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Demo 3 | 4 | final class DemoTests: XCTestCase { 5 | func testHello() throws { 6 | XCTAssertEqual(Demo.sayHello(), "Hello, world!") 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/package_resolved_fixtures/valid.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "5ca84ad21a87cfff30163bd7dd55abded567e0600b5a0adbc80cfe0c687ecc69", 3 | "pins" : [ 4 | { 5 | "identity" : "screenobject", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/Automattic/ScreenObject", 8 | "state" : { 9 | "revision" : "328db56c62aab91440ec5e07cc9f7eef6e26a26e", 10 | "version" : "0.2.3" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/project/.gitignore: -------------------------------------------------------------------------------- 1 | # For convenience with the code generation, ignore everything in this folder other that what's required to code-gen and test behavior 2 | * 3 | !project.yml 4 | !Makefile 5 | !Demo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved 6 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/project/Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := generate 2 | 3 | generate: 4 | xcodegen generate 5 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/project/project.yml: -------------------------------------------------------------------------------- 1 | name: Demo 2 | options: 3 | bundleIdPrefix: com.automattic.tests 4 | packages: 5 | ScreenObject: 6 | url: https://github.com/Automattic/ScreenObject 7 | from: 0.2.3 8 | targets: 9 | Demo: 10 | type: framework 11 | platform: iOS 12 | sources: [../package/Sources] 13 | settings: 14 | GENERATE_INFOPLIST_FILE: YES 15 | scheme: 16 | testTargets: [DemoTests] 17 | DemoTests: 18 | target: Demo 19 | type: bundle.unit-test 20 | platform: iOS 21 | sources: [../package/Tests/] 22 | settings: 23 | GENERATE_INFOPLIST_FILE: YES 24 | # No need for code signing in this demo, plus, it's the test target 25 | CODE_SIGNING_ALLOWED: NO 26 | dependencies: 27 | - target: Demo 28 | - package: ScreenObject 29 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/test_scripts/run_tests_with_xcodebuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | echo "--- :xcode: Run tests to verify packages have been fetched and are available" 6 | xcodebuild test \ 7 | -scheme Demo \ 8 | -configuration Debug \ 9 | -destination 'platform=iOS Simulator' \ 10 | | xcbeautify 11 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/test_scripts/set_up_environment.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "${BASH_SOURCE[0]}" -ef "$0" ]; then 4 | echo "$0 must be sourced" 5 | exit 1 6 | fi 7 | 8 | set -eu 9 | 10 | echo "--- :computer: Prepare environment" 11 | TESTS_LOCATION="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" 12 | NEW_PATH=$PATH:"$TESTS_LOCATION/../../../bin" 13 | 14 | export PATH=$NEW_PATH 15 | export TESTS_LOCATION 16 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/test_scripts/test_package_automatic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/set_up_environment.sh" 6 | 7 | echo "--- :computer: Jump to test folder" 8 | pushd "$TESTS_LOCATION/../package" 9 | 10 | echo "--- :wrench: Run install_swiftpm_dependencies" 11 | install_swiftpm_dependencies 12 | 13 | echo "--- :xcode: Run tests to verify packages have been fetched and are available" 14 | swift test 15 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/test_scripts/test_package_explicit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/set_up_environment.sh" 6 | 7 | echo "--- :computer: Jump to test folder" 8 | pushd "$TESTS_LOCATION/../package" 9 | 10 | echo "--- :wrench: Run install_swiftpm_dependencies" 11 | install_swiftpm_dependencies --use-spm 12 | 13 | echo "--- :xcode: Run tests to verify packages have been fetched and are available" 14 | swift test 15 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/test_scripts/test_project_automatic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/set_up_environment.sh" 6 | 7 | echo "--- :computer: Jump to test folder" 8 | pushd "$TESTS_LOCATION/../project" 9 | 10 | echo "--- :computer: Generate project" 11 | brew install xcodegen 12 | make 13 | 14 | echo "--- :computer: Copy Package.resolved fixture" 15 | PROJECT="Demo.xcodeproj" 16 | XCODE_SPM_PATH="$PROJECT/project.xcworkspace/xcshareddata/swiftpm" 17 | mkdir -p "$XCODE_SPM_PATH" 18 | cp "$TESTS_LOCATION/../package_resolved_fixtures/valid.resolved" "$XCODE_SPM_PATH/Package.resolved" 19 | 20 | echo "--- :wrench: Run install_swiftpm_dependencies" 21 | install_swiftpm_dependencies 22 | 23 | "$TESTS_LOCATION/run_tests_with_xcodebuild.sh" 24 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/test_scripts/test_project_explicit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/set_up_environment.sh" 6 | 7 | echo "--- :computer: Jump to test folder" 8 | pushd "$TESTS_LOCATION/../project" 9 | 10 | echo "--- :computer: Generate project" 11 | brew install xcodegen 12 | make 13 | 14 | echo "--- :computer: Copy Package.resolved fixture" 15 | PROJECT="Demo.xcodeproj" 16 | XCODE_SPM_PATH="$PROJECT/project.xcworkspace/xcshareddata/swiftpm" 17 | mkdir -p "$XCODE_SPM_PATH" 18 | cp "$TESTS_LOCATION/../package_resolved_fixtures/valid.resolved" "$XCODE_SPM_PATH/Package.resolved" 19 | 20 | echo "--- :wrench: Run install_swiftpm_dependencies" 21 | install_swiftpm_dependencies --project $PROJECT 22 | 23 | "$TESTS_LOCATION/run_tests_with_xcodebuild.sh" 24 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/test_scripts/test_project_no_package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/set_up_environment.sh" 6 | 7 | echo "--- :computer: Jump to test folder" 8 | pushd "$TESTS_LOCATION/../project" 9 | 10 | echo "--- :computer: Generate project" 11 | brew install xcodegen 12 | make 13 | 14 | # Notice we do not set up the fixture Package.resolved. 15 | # As such, we expect the call to the plugin to fail. 16 | 17 | echo "--- :wrench: Run install_swiftpm_dependencies" 18 | PROJECT=Demo.xcodeproj 19 | LOGS_PATH=logs 20 | set +e 21 | install_swiftpm_dependencies --project $PROJECT 2>&1 | tee "$LOGS_PATH" 22 | CMD_EXIT_STATUS=$? 23 | set -e 24 | 25 | EXPECTED="Unable to find \`Package.resolved\` file ($PROJECT/project.xcworkspace/xcshareddata/swiftpm/Package.resolved)" 26 | 27 | if [[ $CMD_EXIT_STATUS -eq 0 ]]; then 28 | echo "+++ :x: install_swiftpm_dependencies unexpectedly succeeded without a Package.resolved in the project folder!" 29 | echo "Expected: $EXPECTED" 30 | echo "Got: $(cat $LOGS_PATH)" 31 | exit 1 32 | else 33 | if grep -qF "$EXPECTED" "$LOGS_PATH"; then 34 | echo "^^^ +++" 35 | echo "+++ :white_check_mark: install_swiftpm_dependencies failed as expected because there is no Package.resolved in the project folder." 36 | else 37 | echo "+++ :x: install_swiftpm_dependencies failed, but the message it printed is not what we expected." 38 | echo "Expected: $EXPECTED" 39 | echo "Got: $(cat $LOGS_PATH)" 40 | exit 1 41 | fi 42 | fi 43 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/test_scripts/test_workspace_and_project_automatic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/set_up_environment.sh" 6 | 7 | echo "--- :computer: Jump to test folder" 8 | pushd "$TESTS_LOCATION/../workspace_and_project" 9 | 10 | echo "--- :computer: Generate project" 11 | brew install xcodegen 12 | make 13 | 14 | echo "--- :wrench: Run install_swiftpm_dependencies" 15 | install_swiftpm_dependencies 16 | 17 | "$TESTS_LOCATION/run_tests_with_xcodebuild.sh" 18 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/test_scripts/test_workspace_and_project_explicit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/set_up_environment.sh" 6 | 7 | echo "--- :computer: Jump to test folder" 8 | pushd "$TESTS_LOCATION/../workspace_and_project" 9 | 10 | echo "--- :computer: Generate project" 11 | brew install xcodegen 12 | make 13 | 14 | echo "--- :wrench: Run install_swiftpm_dependencies" 15 | install_swiftpm_dependencies --workspace Demo.xcworkspace 16 | 17 | "$TESTS_LOCATION/run_tests_with_xcodebuild.sh" 18 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/test_scripts/test_workspace_automatic.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/set_up_environment.sh" 6 | 7 | echo "--- :computer: Jump to test folder" 8 | pushd "$TESTS_LOCATION/../workspace" 9 | 10 | echo "--- :computer: Generate project" 11 | brew install xcodegen 12 | make 13 | 14 | echo "--- :wrench: Run install_swiftpm_dependencies" 15 | install_swiftpm_dependencies 16 | 17 | "$TESTS_LOCATION/run_tests_with_xcodebuild.sh" 18 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/test_scripts/test_workspace_explicit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/set_up_environment.sh" 6 | 7 | echo "--- :computer: Jump to test folder" 8 | pushd "$TESTS_LOCATION/../workspace" 9 | 10 | echo "--- :computer: Generate project" 11 | brew install xcodegen 12 | make 13 | 14 | echo "--- :wrench: Run install_swiftpm_dependencies" 15 | install_swiftpm_dependencies --workspace Demo.xcworkspace 16 | 17 | "$TESTS_LOCATION/run_tests_with_xcodebuild.sh" 18 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/workspace/.gitignore: -------------------------------------------------------------------------------- 1 | # For convenience with the code generation, ignore everything other than the Makefile and workspace 2 | * 3 | !Makefile 4 | !*.xcworkspace/ 5 | *.xcworkspace/xcuserdata 6 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/workspace/Demo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/workspace/Demo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/workspace/Demo.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "df0bf1b75530ea9424408e39d4acdb54dd26151176343ec90d9352ede0d6529e", 3 | "pins" : [ 4 | { 5 | "identity" : "screenobject", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/automattic/screenobject", 8 | "state" : { 9 | "revision" : "328db56c62aab91440ec5e07cc9f7eef6e26a26e", 10 | "version" : "0.2.3" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/workspace/Makefile: -------------------------------------------------------------------------------- 1 | generate: 2 | xcodegen generate --spec ../project/project.yml 3 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/workspace_and_project/.gitignore: -------------------------------------------------------------------------------- 1 | # For convenience with the code generation, ignore everything other than the Makefile and workspace 2 | * 3 | !Makefile 4 | !*.xcworkspace/ 5 | *.xcworkspace/xcuserdata 6 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/workspace_and_project/Demo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/workspace_and_project/Demo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/workspace_and_project/Demo.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "df0bf1b75530ea9424408e39d4acdb54dd26151176343ec90d9352ede0d6529e", 3 | "pins" : [ 4 | { 5 | "identity" : "screenobject", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/automattic/screenobject", 8 | "state" : { 9 | "revision" : "328db56c62aab91440ec5e07cc9f7eef6e26a26e", 10 | "version" : "0.2.3" 11 | } 12 | } 13 | ], 14 | "version" : 3 15 | } 16 | -------------------------------------------------------------------------------- /tests/install_swiftpm_dependencies/workspace_and_project/Makefile: -------------------------------------------------------------------------------- 1 | generate: 2 | # Note: we need to copy the project file in place (as opposed to using --spec) 3 | # so that xcodegen can add the scheme for the Demo project into the `Demo.xcworkspace` 4 | cp ../project/project.yml . 5 | xcodegen generate 6 | -------------------------------------------------------------------------------- /tests/pr_changed_files/test_all_match_patterns.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -u 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/test_helpers.sh" 6 | 7 | echo "--- :git: Testing all-match pattern matching" 8 | 9 | # Create test repository 10 | repo_path=$(create_tmp_repo_dir) 11 | trap 'cleanup_git_repo "$repo_path"' EXIT 12 | 13 | # Set up environment variables 14 | export BUILDKITE_PULL_REQUEST="123" 15 | export BUILDKITE_PULL_REQUEST_BASE_BRANCH="base" 16 | 17 | # Initialize the repository 18 | init_test_repo "$repo_path" 19 | 20 | # Create test files (using single quotes to avoid special chars being interpreted by the shell) 21 | mkdir -p docs src/swift 22 | echo "doc1" > 'docs/read me.md' 23 | echo "doc2" > 'docs/guide with spaces.md' 24 | echo "doc3" > 'docs/special\!@*#$chars.md' 25 | git add . 26 | git commit -m "Add doc files" 27 | 28 | # [Test] All changes in docs - exit code only 29 | output=$(pr_changed_files --all-match 'docs/*') 30 | result=$? 31 | assert_result $result 0 "$output" "" "Should match when all changes are in docs" 32 | 33 | # Test with stdout 34 | output=$(pr_changed_files --stdout --all-match 'docs/*') 35 | result=$? 36 | assert_result $result 0 "$output" "true" "Should match when all changes are in docs with --stdout" 37 | 38 | # [Test] All changes in docs with explicit patterns including spaces and special chars - exit code only 39 | output=$(pr_changed_files --all-match 'docs/read me.md' 'docs/guide with spaces.md' 'docs/special\\!@\*#$chars.md') 40 | result=$? 41 | assert_result $result 0 "$output" "" "Should match when all changes match patterns with spaces and special chars" 42 | 43 | # Test with stdout 44 | output=$(pr_changed_files --stdout --all-match 'docs/read me.md' 'docs/guide with spaces.md' 'docs/special\\!@\*#$chars.md') 45 | result=$? 46 | assert_result $result 0 "$output" "true" "Should match when all changes match patterns with spaces and special chars with --stdout" 47 | 48 | # [Test] All changes in docs with globbing patterns including spaces and special chars - exit code only 49 | output=$(pr_changed_files --all-match 'docs/read me.md' 'docs/guide with spaces.md' 'docs/special\\!*.md') 50 | result=$? 51 | assert_result $result 0 "$output" "" "Should match when all changes match patterns with globbing" 52 | 53 | # Test with stdout 54 | output=$(pr_changed_files --stdout --all-match 'docs/read me.md' 'docs/guide with spaces.md' 'docs/special\\!*.md') 55 | result=$? 56 | assert_result $result 0 "$output" "true" "Should match when all changes match patterns with globbing with --stdout" 57 | 58 | # [Test] Changes outside pattern - exit code only 59 | echo "swift" > 'src/swift/main with spaces.swift' 60 | echo "swift" > 'src/swift/special!\@#*$chars.swift' 61 | git add . 62 | git commit -m "Add swift file" 63 | 64 | output=$(pr_changed_files --all-match 'docs/*') 65 | result=$? 66 | assert_result $result 1 "$output" "" "Should not match when changes exist outside patterns" 67 | 68 | # Test with stdout 69 | output=$(pr_changed_files --stdout --all-match 'docs/*') 70 | result=$? 71 | assert_result $result 0 "$output" "false" "Should not match when changes exist outside patterns with --stdout" 72 | 73 | # [Test] Multiple patterns, all matching - exit code only 74 | output=$(pr_changed_files --all-match 'docs/*' 'src/swift/main with spaces.swift' 'src/swift/special\!\\@#\*$chars.swift') 75 | result=$? 76 | assert_result $result 0 "$output" "" "Should match when all changes match multiple patterns" 77 | 78 | # Test with stdout 79 | output=$(pr_changed_files --stdout --all-match 'docs/*' 'src/swift/main with spaces.swift' 'src/swift/special\!\\@#\*$chars.swift') 80 | result=$? 81 | assert_result $result 0 "$output" "true" "Should match when all changes match multiple patterns with --stdout" 82 | 83 | # [Test] Multiple patterns, all matching, including some using globbing - exit code only 84 | output=$(pr_changed_files --all-match 'docs/*' 'src/swift/main with spaces.swift' 'src/swift/special*chars.swift') 85 | result=$? 86 | assert_result $result 0 "$output" "" "Should match when all changes match patterns with globbing" 87 | 88 | # Test with stdout 89 | output=$(pr_changed_files --stdout --all-match 'docs/*' 'src/swift/main with spaces.swift' 'src/swift/special*chars.swift') 90 | result=$? 91 | assert_result $result 0 "$output" "true" "Should match when all changes match patterns with globbing with --stdout" 92 | 93 | echo "✅ All-match pattern tests passed" 94 | -------------------------------------------------------------------------------- /tests/pr_changed_files/test_any_match_patterns.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -u 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/test_helpers.sh" 6 | 7 | echo "--- :git: Testing any-match pattern matching" 8 | 9 | # Create test repository 10 | repo_path=$(create_tmp_repo_dir) 11 | trap 'cleanup_git_repo "$repo_path"' EXIT 12 | 13 | # Set up environment variables 14 | export BUILDKITE_PULL_REQUEST="123" 15 | export BUILDKITE_PULL_REQUEST_BASE_BRANCH="base" 16 | 17 | # Initialize the repository 18 | init_test_repo "$repo_path" 19 | 20 | # Create test files (using single quotes to avoid special chars being interpreted by the shell) 21 | mkdir -p docs src/swift src/ruby 22 | echo "doc" > 'docs/read me.md' 23 | echo "doc" > 'docs/special!@*#$chars.md' 24 | echo "swift" > 'src/swift/main.swift' 25 | echo "ruby" > 'src/ruby/main.rb' 26 | git add . 27 | git commit -m "Add test files" 28 | 29 | # [Test] Match specific extension - exit code only 30 | output=$(pr_changed_files --any-match '*.swift') 31 | result=$? 32 | assert_result $result 0 "$output" "" "Should match .swift files" 33 | 34 | # Test with stdout 35 | output=$(pr_changed_files --stdout --any-match '*.swift') 36 | result=$? 37 | assert_result $result 0 "$output" "true" "Should match .swift files with --stdout" 38 | 39 | # [Test] Match multiple patterns - exit code only 40 | output=$(pr_changed_files --any-match 'docs/*.md' '*.rb') 41 | result=$? 42 | assert_result $result 0 "$output" "" "Should match multiple patterns" 43 | 44 | # Test with stdout 45 | output=$(pr_changed_files --stdout --any-match 'docs/*.md' '*.rb') 46 | result=$? 47 | assert_result $result 0 "$output" "true" "Should match multiple patterns with --stdout" 48 | 49 | # [Test] Match files with spaces and special characters - exit code only 50 | output=$(pr_changed_files --any-match 'docs/read me.md' 'docs/special!@*#$chars.md') 51 | result=$? 52 | assert_result $result 0 "$output" "" "Should match files with spaces and special characters" 53 | 54 | # Test with stdout 55 | output=$(pr_changed_files --stdout --any-match 'docs/read me.md' 'docs/special!@*#$chars.md') 56 | result=$? 57 | assert_result $result 0 "$output" "true" "Should match files with spaces and special characters with --stdout" 58 | 59 | # [Test] No matches - exit code only 60 | output=$(pr_changed_files --any-match '*.js') 61 | result=$? 62 | assert_result $result 1 "$output" "" "Should not match non-existent patterns" 63 | 64 | # Test with stdout 65 | output=$(pr_changed_files --stdout --any-match '*.js') 66 | result=$? 67 | assert_result $result 0 "$output" "false" "Should not match non-existent patterns with --stdout" 68 | 69 | # [Test] Directory pattern - exit code only 70 | output=$(pr_changed_files --any-match 'docs/*') 71 | result=$? 72 | assert_result $result 0 "$output" "" "Should match directory patterns" 73 | 74 | # Test with stdout 75 | output=$(pr_changed_files --stdout --any-match 'docs/*') 76 | result=$? 77 | assert_result $result 0 "$output" "true" "Should match directory patterns with --stdout" 78 | 79 | # [Test] Exact pattern matching - exit code only 80 | echo "swiftfile" > swiftfile.txt 81 | git add swiftfile.txt 82 | git commit -m "Add file with swift in name" 83 | 84 | output=$(pr_changed_files --any-match '*.swift') 85 | result=$? 86 | assert_result $result 0 "$output" "" "Should only match exact patterns" 87 | 88 | # Test with stdout 89 | output=$(pr_changed_files --stdout --any-match '*.swift') 90 | result=$? 91 | assert_result $result 0 "$output" "true" "Should only match exact patterns with --stdout" 92 | 93 | echo "✅ Any-match pattern tests passed" 94 | -------------------------------------------------------------------------------- /tests/pr_changed_files/test_basic_changes.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -u 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/test_helpers.sh" 6 | 7 | echo "--- :git: Testing basic changes detection" 8 | 9 | # Create test repository 10 | repo_path=$(create_tmp_repo_dir) 11 | trap 'cleanup_git_repo "$repo_path"' EXIT 12 | 13 | # Set up environment variables 14 | export BUILDKITE_PULL_REQUEST="123" 15 | export BUILDKITE_PULL_REQUEST_BASE_BRANCH="base" 16 | 17 | # Initialize the repository 18 | init_test_repo "$repo_path" 19 | 20 | # [Test] No changes - exit code only 21 | output=$(pr_changed_files) 22 | result=$? 23 | assert_result $result 1 "$output" "" "Should return 1 when no files changed" 24 | 25 | # [Test] No changes - with stdout 26 | output=$(pr_changed_files --stdout) 27 | result=$? 28 | assert_result $result 0 "$output" "false" "Should output 'false' and return 0 with --stdout when no files changed" 29 | 30 | # [Test] Single file change - exit code only 31 | echo "change" > new.txt 32 | git add new.txt 33 | git commit -m "Add new file" 34 | 35 | output=$(pr_changed_files) 36 | result=$? 37 | assert_result $result 0 "$output" "" "Should return 0 when files changed" 38 | 39 | # [Test] Single file change - with stdout 40 | output=$(pr_changed_files --stdout) 41 | result=$? 42 | assert_result $result 0 "$output" "true" "Should output 'true' and return 0 with --stdout when files changed" 43 | 44 | echo "✅ Basic changes tests passed" 45 | -------------------------------------------------------------------------------- /tests/pr_changed_files/test_edge_cases.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -u 2 | 3 | set -o pipefail 4 | 5 | source "$(dirname "${BASH_SOURCE[0]}")/test_helpers.sh" 6 | 7 | echo "--- :git: Testing edge cases" 8 | 9 | # Create test repository 10 | repo_path=$(create_tmp_repo_dir) 11 | trap 'cleanup_git_repo "$repo_path"' EXIT 12 | 13 | # Set up environment variables 14 | export BUILDKITE_PULL_REQUEST="123" 15 | export BUILDKITE_PULL_REQUEST_BASE_BRANCH="base" 16 | 17 | # Initialize the repository 18 | init_test_repo "$repo_path" 19 | 20 | # [Test] Invalid PR environment 21 | unset BUILDKITE_PULL_REQUEST 22 | output=$(pr_changed_files 2>&1) 23 | result=$? 24 | assert_result $result 255 "$output" "Error: this tool can only be called from a Buildkite PR job" "Should fail when not in PR environment" 25 | 26 | export BUILDKITE_PULL_REQUEST="123" 27 | 28 | # [Test] No patterns provided 29 | output=$(pr_changed_files --any-match 2>&1) 30 | result=$? 31 | assert_result $result 255 "$output" "Error: must specify at least one file pattern" "Should fail when no patterns provided" 32 | 33 | # [Test] Flag followed by another flag 34 | output=$(pr_changed_files --any-match --something 2>&1) 35 | result=$? 36 | assert_result $result 255 "$output" "Error: must specify at least one file pattern" "Should fail with correct error when flag is followed by another flag" 37 | 38 | # [Test] Mutually exclusive options 39 | output=$(pr_changed_files --any-match "*.txt" --all-match "*.md" 2>&1) 40 | result=$? 41 | assert_result $result 255 "$output" "Error: either specify --all-match or --any-match; cannot specify both" "Should fail with correct error when using mutually exclusive options" 42 | 43 | # [Test] Files with spaces and special characters 44 | mkdir -p 'folder with spaces/nested!\@*#$folder' 45 | echo "test" > 'folder with spaces/file with spaces.txt' 46 | echo "test" > 'folder with spaces/nested!\@*#$folder/file_with_!@*#$chars.txt' 47 | git add . 48 | git commit -m "Add files with special characters" 49 | 50 | # Test with exit code only 51 | output=$(pr_changed_files) 52 | result=$? 53 | assert_result $result 0 "$output" "" "Should handle files with spaces and special characters" 54 | 55 | # Test with stdout 56 | output=$(pr_changed_files --stdout) 57 | result=$? 58 | assert_result $result 0 "$output" "true" "Should handle files with spaces and special characters with --stdout" 59 | 60 | # [Test] Pattern matching with spaces and special characters - exit code only 61 | output=$(pr_changed_files --any-match '*spaces.txt') 62 | result=$? 63 | assert_result $result 0 "$output" "" "Should match files with special characters in path" 64 | 65 | # Test with stdout 66 | output=$(pr_changed_files --stdout --any-match '*spaces.txt') 67 | result=$? 68 | assert_result $result 0 "$output" "true" "Should match files with special characters in path with --stdout" 69 | 70 | # [Test] No changes between branches - exit code only 71 | git checkout -b no_changes base 72 | output=$(pr_changed_files) 73 | result=$? 74 | assert_result $result 1 "$output" "" "Should handle no changes between branches" 75 | 76 | # Test with stdout 77 | output=$(pr_changed_files --stdout) 78 | result=$? 79 | assert_result $result 0 "$output" "false" "Should handle no changes between branches with --stdout" 80 | 81 | # [Test] Empty commit - exit code only 82 | git checkout -b empty_commit base 83 | git commit --allow-empty -m "Empty commit" 84 | output=$(pr_changed_files) 85 | result=$? 86 | assert_result $result 1 "$output" "" "Should handle empty commit" 87 | 88 | # Test with stdout 89 | output=$(pr_changed_files --stdout) 90 | result=$? 91 | assert_result $result 0 "$output" "false" "Should handle empty commit with --stdout" 92 | 93 | # [Test] Empty repository state - exit code only 94 | git checkout --orphan empty 95 | git rm -rf . 96 | git commit --allow-empty -m "Empty initial commit" 97 | 98 | output=$(pr_changed_files) 99 | result=$? 100 | assert_result $result 1 "$output" "" "Should handle empty repository state" 101 | 102 | # Test with stdout 103 | output=$(pr_changed_files --stdout) 104 | result=$? 105 | assert_result $result 0 "$output" "false" "Should handle empty repository state with --stdout" 106 | 107 | echo -e "\n✅ Edge cases tests passed" 108 | -------------------------------------------------------------------------------- /tests/pr_changed_files/test_helpers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | set -o pipefail 4 | 5 | # Add bin directory to PATH 6 | REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" 7 | export PATH="$REPO_ROOT/bin:$PATH" 8 | 9 | # Create a temporary git repository for testing 10 | create_tmp_repo_dir() { 11 | local temp_dir 12 | temp_dir=$(mktemp -d) 13 | echo "$temp_dir" 14 | } 15 | 16 | # Initialize the test repository 17 | init_test_repo() { 18 | local repo_dir="$1" 19 | ORIGINAL_DIR=$(pwd) 20 | 21 | # Create a bare repo to act as remote 22 | mkdir -p "$repo_dir/remote" 23 | git init --bare "$repo_dir/remote" 24 | 25 | # Create the working repo 26 | mkdir -p "$repo_dir/local" 27 | pushd "$repo_dir/local" 28 | 29 | # Initialize git repo 30 | git init 31 | git config user.email "test@example.com" 32 | git config user.name "Test User" 33 | 34 | # Add remote 35 | git remote add origin "$repo_dir/remote" 36 | 37 | # Create and commit initial files on main branch 38 | echo "initial" > initial.txt 39 | git add initial.txt 40 | git commit -m "Initial commit" 41 | 42 | # Create base branch 43 | git checkout -b base 44 | echo "base" > base.txt 45 | git add base.txt 46 | git commit -m "Base branch commit" 47 | 48 | # Push base branch to remote 49 | git push -u origin base 50 | 51 | # Create PR branch 52 | git checkout -b pr 53 | } 54 | 55 | # Clean up the temporary repository 56 | cleanup_git_repo() { 57 | # Return to original directory if we're still in the temp dir 58 | if [[ "$(pwd)" == "$1/local" ]]; then 59 | cd "$ORIGINAL_DIR" 60 | fi 61 | rm -rf "$1" 62 | } 63 | 64 | # Helper to assert both return code and output 65 | # Arguments: 66 | # $1 - Actual return code 67 | # $2 - Expected return code 68 | # $3 - Actual output 69 | # $4 - Expected output 70 | # $5 - Optional message to display with the assertion result 71 | assert_result() { 72 | local actual_code="$1" 73 | local expected_code="$2" 74 | local actual_output="$3" 75 | local expected_output="$4" 76 | local message="$5" 77 | 78 | assert_equal "$actual_code" "$expected_code" "Exit code - $message" 79 | assert_equal "$actual_output" "$expected_output" "Output - $message" 80 | } 81 | 82 | # Helper function to assert that two values are equal 83 | # Arguments: 84 | # $1 - Expected value 85 | # $2 - Actual value 86 | # $3 - Optional message to display with the assertion result 87 | assert_equal() { 88 | local actual="$1" 89 | local expected="$2" 90 | local message="${3:-}" 91 | 92 | if [[ "$actual" == "$expected" ]]; then 93 | echo "🟢 Assertion ('$actual') succeeded: $message" 94 | elif [[ "$actual" != "$expected" ]]; then 95 | echo "❌ Assertion failed ($actual != $expected): $message" 96 | echo "Expected: $expected" 97 | echo "Actual : $actual" 98 | exit 1 99 | fi 100 | } 101 | -------------------------------------------------------------------------------- /tests/test_craft_comment_with_dependency_diff.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | setup() { 4 | # Get the directory of the test file 5 | DIR="$( cd "$( dirname "$BATS_TEST_FILENAME" )" >/dev/null 2>&1 && pwd )" 6 | 7 | # Create a temporary directory for test artifacts 8 | export TEMP_DIR="$(mktemp -d)" 9 | export DIFF_DEPENDENCIES_FOLDER="$TEMP_DIR" 10 | export COMMENT_FILE="$DIFF_DEPENDENCIES_FOLDER/comment_body.txt" 11 | export TLDR_DIFF_DEPENDENCIES_FILE="$TEMP_DIR/tldr_dependencies.txt" 12 | export TLDR_DIFF_BUILD_ENV_FILE="$TEMP_DIR/tldr_build_env.txt" 13 | export DIFF_DEPENDENCIES_FILE="$TEMP_DIR/dependencies.txt" 14 | export DIFF_BUILD_ENV_FILE="$TEMP_DIR/build_env.txt" 15 | 16 | # Mock Buildkite environment variables 17 | export BUILDKITE_BUILD_URL="https://buildkite.com/test/build" 18 | export BUILDKITE_JOB_ID="job123" 19 | } 20 | 21 | teardown() { 22 | rm -rf "$TEMP_DIR" 23 | } 24 | 25 | @test "should create comment with both TLDRs and trees when content is small" { 26 | # Create test dependency files with small content 27 | echo "org.test:library:2.0.0 (from 1.0.0)" > "$TLDR_DIFF_DEPENDENCIES_FILE" 28 | echo "org.test:build-tool:3.0.0 (from 2.0.0)" > "$TLDR_DIFF_BUILD_ENV_FILE" 29 | 30 | echo "+org.test:library:2.0.0 31 | -org.test:library:1.0.0" > "$DIFF_DEPENDENCIES_FILE" 32 | 33 | echo "+org.test:build-tool:3.0.0 34 | -org.test:build-tool:2.0.0" > "$DIFF_BUILD_ENV_FILE" 35 | 36 | # Run the script 37 | run "$DIR/../bin/craft_comment_with_dependency_diff" 38 | 39 | # Assert success 40 | [ "$status" -eq 0 ] 41 | 42 | # Read generated comment 43 | local comment_content=$(<"$COMMENT_FILE") 44 | 45 | # Assert both sections are present 46 | [[ "$comment_content" =~ "## Project dependencies changes" ]] 47 | [[ "$comment_content" =~ "## Build environment changes" ]] 48 | 49 | # Assert both TLDRs are present 50 | [[ "$comment_content" =~ "org.test:library:2.0.0 (from 1.0.0)" ]] 51 | [[ "$comment_content" =~ "org.test:build-tool:3.0.0 (from 2.0.0)" ]] 52 | 53 | # Assert both trees are present 54 | [[ "$comment_content" =~ "+org.test:library:2.0.0" ]] 55 | [[ "$comment_content" =~ "+org.test:build-tool:3.0.0" ]] 56 | 57 | # Assert no "too large" warnings 58 | [[ ! "$comment_content" =~ "tree is too large" ]] 59 | } 60 | 61 | @test "should handle mixed tree sizes with large project dependencies" { 62 | # Create large project dependencies tree 63 | echo "org.test:library:2.0.0 (from 1.0.0)" > "$TLDR_DIFF_DEPENDENCIES_FILE" 64 | 65 | # Generate a large tree for project dependencies 66 | local large_tree="" 67 | for i in {1..1100}; do 68 | large_tree+="+org.test:large-lib-${i}:2.0.0 69 | -org.test:large-lib-${i}:1.0.0 70 | " 71 | done 72 | echo "$large_tree" > "$DIFF_DEPENDENCIES_FILE" 73 | 74 | # Create small build environment changes 75 | echo "org.test:build-tool:3.0.0 (from 2.0.0)" > "$TLDR_DIFF_BUILD_ENV_FILE" 76 | echo "+org.test:build-tool:3.0.0 77 | -org.test:build-tool:2.0.0" > "$DIFF_BUILD_ENV_FILE" 78 | 79 | # Run the script 80 | run "$DIR/../bin/craft_comment_with_dependency_diff" 81 | 82 | # Assert success 83 | [ "$status" -eq 0 ] 84 | 85 | # Read generated comment 86 | local comment_content=$(<"$COMMENT_FILE") 87 | 88 | echo "$comment_content"; 89 | 90 | # Assert both sections are present 91 | [[ "$comment_content" =~ "## Project dependencies changes" ]] 92 | [[ "$comment_content" =~ "## Build environment changes" ]] 93 | 94 | # Assert both TLDRs are present 95 | [[ "$comment_content" =~ "org.test:library:2.0.0 (from 1.0.0)" ]] 96 | [[ "$comment_content" =~ "org.test:build-tool:3.0.0 (from 2.0.0)" ]] 97 | 98 | # Assert small build environment tree is present 99 | [[ "$comment_content" =~ "+org.test:build-tool:3.0.0" ]] 100 | [[ "$comment_content" =~ "-org.test:build-tool:2.0.0" ]] 101 | 102 | # Assert large project dependencies tree is replaced with warning 103 | [[ "$comment_content" =~ "Project dependencies tree is too large" ]] 104 | [[ "$comment_content" =~ "View it in Buildkite artifacts" ]] 105 | 106 | # Assert only one "too large" warning 107 | [[ ! "$comment_content" =~ "Build environment tree is too large" ]] 108 | } 109 | -------------------------------------------------------------------------------- /tests/test_install_windows_10_sdk.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [int]$ExpectedExitCode = 0, 3 | [string]$ExpectedErrorKeyphrase = "" 4 | ) 5 | 6 | # Ensure the output is UTF-8 encoded so we can use emojis... 7 | [System.Console]::OutputEncoding = [System.Text.Encoding]::UTF8 8 | $emojiGreenCheck = "$([char]0x2705)" 9 | $emojiRedCross = "$([char]0x274C)" 10 | 11 | Write-Output "Running $($MyInvocation.MyCommand.Name) with ExpectedExitCode=$ExpectedExitCode and ExpectedErrorKeyphrase=$ExpectedErrorKeyphrase" 12 | 13 | if (($ExpectedExitCode -eq 0) -and ($ExpectedErrorKeyphrase -ne "")) { 14 | Write-Output "$emojiRedCross Expected call to succeed (expected error code = 0), but given an error keyphrase to check." 15 | exit 1 16 | } 17 | 18 | $output = & "$PSScriptRoot\..\bin\install_windows_10_sdk.ps1" -DryRun 19 | $exitCode = $LASTEXITCODE 20 | 21 | if ($exitCode -ne $ExpectedExitCode) { 22 | Write-Output "$emojiRedCross Expected exit code $ExpectedExitCode, got $exitCode" 23 | Write-Output "Output was:" 24 | Write-Output "$output" 25 | exit 1 26 | } else { 27 | Write-Output "$emojiGreenCheck Exit code matches expected value ($ExpectedExitCode)" 28 | } 29 | 30 | # Only check error keyphrase if exit code is not 0 31 | if ($exitCode -eq 0) { 32 | exit 0 33 | } 34 | 35 | # If keyphrase is empty, assume the caller is satisfied with only testing the exit code 36 | if ($ExpectedErrorKeyphrase -eq "") { 37 | Write-Output "Exit code match expectation and no error keyphrase was provided. Test completed." 38 | exit 0 39 | } 40 | 41 | if ($output -match [regex]::Escape($ExpectedErrorKeyphrase)) { 42 | Write-Output "$emojiGreenCheck Error keyphrase matches expected value ($ExpectedErrorKeyphrase)" 43 | Write-Output "Test completed." 44 | } else { 45 | Write-Output "$emojiRedCross Expected error to contain '$ExpectedErrorKeyphrase', but got:" 46 | Write-Output "$output" 47 | exit 1 48 | } 49 | -------------------------------------------------------------------------------- /tests/test_prepare_windows_host_for_app_distribution.ps1: -------------------------------------------------------------------------------- 1 | # Tests the prepare_windows_host_for_app_distribution.ps1 script with the -SkipWindows10SDKInstallation parameter. 2 | # 3 | # We only test the skip behavior because the installation takes a "long" time to run. 4 | 5 | param ( 6 | [int]$ExpectedExitCode = 0 7 | ) 8 | 9 | # Ensure the output is UTF-8 encoded so we can use emojis... 10 | [System.Console]::OutputEncoding = [System.Text.Encoding]::UTF8 11 | $emojiGreenCheck = "$([char]0x2705)" 12 | $emojiRedCross = "$([char]0x274C)" 13 | 14 | Write-Output "Testing prepare_windows_host_for_app_distribution.ps1 with -SkipWindows10SDKInstallation" 15 | 16 | # Create a valid SDK version file to ensure it's not being used 17 | $sdkVersion = "20348" 18 | "$sdkVersion" | Out-File .windows-10-sdk-version 19 | 20 | # Run the script with skip parameter 21 | $output = & "$PSScriptRoot\..\bin\prepare_windows_host_for_app_distribution.ps1" -SkipWindows10SDKInstallation 22 | $exitCode = $LASTEXITCODE 23 | 24 | # Check exit code 25 | if ($exitCode -ne $ExpectedExitCode) { 26 | Write-Output "$emojiRedCross Expected exit code $ExpectedExitCode, got $exitCode" 27 | Write-Output "Output was:" 28 | Write-Output "$output" 29 | exit 1 30 | } else { 31 | Write-Output "$emojiGreenCheck Exit code matches expected value ($ExpectedExitCode)" 32 | } 33 | 34 | $expectedSkipMessage = "Run with SkipWindows10SDKInstallation = true. Skipping Windows 10 SDK installation check." 35 | if ($output -match [regex]::Escape($expectedSkipMessage)) { 36 | Write-Output "$emojiGreenCheck Found expected skip message in output" 37 | } else { 38 | Write-Output "$emojiRedCross Expected to find message about skipping due to parameter, but got:" 39 | Write-Output "$output" 40 | exit 1 41 | } 42 | 43 | # Verify SDK was not installed by checking the file system 44 | $windowsSDKsRoot = "C:\Program Files (x86)\Windows Kits\10\bin" 45 | $sdkPath = "$windowsSDKsRoot\10.0.$sdkVersion\x64" 46 | If (Test-Path $sdkPath) { 47 | Write-Output "$emojiRedCross Found SDK installation at $sdkPath when it should have been skipped" 48 | exit 1 49 | } else { 50 | Write-Output "$emojiGreenCheck Confirmed SDK was not installed at $sdkPath" 51 | } 52 | 53 | Write-Output "Test completed successfully." 54 | -------------------------------------------------------------------------------- /tests/test_that_all_files_are_executable.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'rspec' 5 | 6 | RSpec.configure do |config| 7 | config.formatter = :documentation 8 | end 9 | 10 | # You might be thinking "why is this in Ruby instead of `environment.bats`?" Good question – for some reason, 11 | # it seems that running `[ -x ]` under `bats` in Docker on a Mac returns invalid results, and this was more reliable. 12 | # 13 | # See: https://github.com/Automattic/a8c-ci-toolkit-buildkite-plugin/pull/42 14 | context 'All Unix Commands Should Be Executable' do 15 | Dir 16 | .children('bin') 17 | # Ignore Windows PowerShell scripts 18 | .reject { |f| f.end_with?('.ps1') } 19 | .map { |f| File.new(File.join('bin', f)) }.each do |file| 20 | it file.path do 21 | expect(file.stat.executable?).to be true 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /tests/validate_get_libc_version_format: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | case $OSTYPE in 4 | linux*) 5 | os_exp="linux" 6 | ;; 7 | darwin*) 8 | os_exp="macos" 9 | ;; 10 | msys*|cygwin*|Windows_NT*) 11 | os_exp="win" 12 | ;; 13 | *) 14 | echo "Unknown OS type: $OSTYPE" 15 | exit 1 16 | ;; 17 | esac 18 | echo "Detected OS type: $OSTYPE" 19 | 20 | libc_version=$(bin/get_libc_version) 21 | echo "Computed libc version is $libc_version" 22 | exp="$os_exp-[0-9]+\.[0-9]+(\.[0-9]+)?" 23 | 24 | if [[ "$libc_version" =~ ^$exp ]]; then 25 | echo "Computed libc version ($libc_version) format matches expected format ($exp)" 26 | else 27 | echo "~~~ :x: Test failed" 28 | echo "^^^ +++ Computed libc version ($libc_version) format does not match expected format ($exp)!" 29 | exit 1 30 | fi 31 | --------------------------------------------------------------------------------