From 1fb0f6078c33fc9ed2dae00d1c483e88db603d13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20S=CC=8Cesta=CC=81k?= Date: Thu, 19 Feb 2026 11:42:14 +0000 Subject: [PATCH 01/10] feat: Add auto-generated documentation site for workflows and actions Build a GitHub Pages documentation site (MkDocs Material) that auto-generates from workflow and action YAML metadata. Covers all 18 reusable workflows and 13 composite actions with usage snippets, input/secret/output tables, and cross-links. Includes enricher plugin API for future AI-powered docs. Co-Authored-By: Claude Opus 4.6 --- .../docs/actions/android/build-firebase.md | 36 +++ .../docs/actions/android/build-googleplay.md | 41 +++ .github/docs/actions/android/check.md | 25 ++ .../android/generate-baseline-profiles.md | 30 ++ .github/docs/actions/android/index.md | 14 + .../docs/actions/android/setup-environment.md | 26 ++ .github/docs/actions/index.md | 12 + .github/docs/actions/ios/export-secrets.md | 24 ++ .github/docs/actions/ios/fastlane-beta.md | 32 +++ .github/docs/actions/ios/fastlane-release.md | 32 +++ .github/docs/actions/ios/fastlane-test.md | 23 ++ .github/docs/actions/ios/index.md | 14 + .github/docs/actions/ios/kmp-build.md | 37 +++ .../utility/detect-changes-changelog.md | 148 ++++++++++ .github/docs/actions/utility/index.md | 12 + .../utility/jira-transition-tickets.md | 120 ++++++++ .../actions/utility/kmp-detect-changes.md | 27 ++ .github/docs/assets/fonts/Roobert-Bold.woff2 | Bin 0 -> 79616 bytes .../docs/assets/fonts/Roobert-Regular.woff2 | Bin 0 -> 77000 bytes .../docs/assets/fonts/Roobert-SemiBold.woff2 | Bin 0 -> 78408 bytes .github/docs/assets/images/favicon.png | Bin 0 -> 374 bytes .github/docs/index.md | 60 ++++ .../docs/overrides/.icons/futured/logo.svg | 8 + .github/docs/stylesheets/extra.css | 176 ++++++++++++ .github/docs/workflows/android/cloud-check.md | 44 +++ .../cloud-generate-baseline-profiles.md | 50 ++++ .../workflows/android/cloud-nightly-build.md | 62 ++++ .../android/cloud-release-firebase.md | 56 ++++ .../android/cloud-release-googleplay.md | 60 ++++ .github/docs/workflows/android/index.md | 14 + .github/docs/workflows/index.md | 14 + .github/docs/workflows/ios-kmp/index.md | 12 + .../workflows/ios-kmp/selfhosted-build.md | 60 ++++ .../workflows/ios-kmp/selfhosted-release.md | 57 ++++ .../docs/workflows/ios-kmp/selfhosted-test.md | 42 +++ .github/docs/workflows/ios/index.md | 14 + .../docs/workflows/ios/selfhosted-build.md | 52 ++++ .../workflows/ios/selfhosted-nightly-build.md | 54 ++++ .../ios/selfhosted-on-demand-build.md | 52 ++++ .../docs/workflows/ios/selfhosted-release.md | 48 ++++ .github/docs/workflows/ios/selfhosted-test.md | 36 +++ .../workflows/kmp/cloud-detect-changes.md | 34 +++ .../workflows/kmp/combined-nightly-build.md | 79 +++++ .github/docs/workflows/kmp/index.md | 11 + .../docs/workflows/universal/cloud-backup.md | 37 +++ .github/docs/workflows/universal/index.md | 12 + .../workflows/universal/selfhosted-backup.md | 37 +++ .../workflows/universal/workflows-lint.md | 12 + .github/mkdocs.yml | 138 +++++++++ .github/requirements-docs.txt | 5 + .github/scripts/config.py | 229 +++++++++++++++ .github/scripts/enrichers/__init__.py | 0 .github/scripts/enrichers/ai_enricher.py | 76 +++++ .github/scripts/enrichers/base.py | 54 ++++ .github/scripts/enrichers/readme_enricher.py | 108 +++++++ .github/scripts/generate-docs.py | 270 ++++++++++++++++++ .github/scripts/parsers/__init__.py | 0 .github/scripts/parsers/action_parser.py | 60 ++++ .github/scripts/parsers/types.py | 46 +++ .github/scripts/parsers/workflow_parser.py | 106 +++++++ .github/scripts/renderers/__init__.py | 0 .../scripts/renderers/markdown_renderer.py | 220 ++++++++++++++ .github/scripts/templates/action.md.j2 | 48 ++++ .github/scripts/templates/index.md.j2 | 14 + .github/scripts/templates/workflow.md.j2 | 89 ++++++ .github/workflows/deploy-docs.yml | 57 ++++ .gitignore | 2 + 67 files changed, 3368 insertions(+) create mode 100644 .github/docs/actions/android/build-firebase.md create mode 100644 .github/docs/actions/android/build-googleplay.md create mode 100644 .github/docs/actions/android/check.md create mode 100644 .github/docs/actions/android/generate-baseline-profiles.md create mode 100644 .github/docs/actions/android/index.md create mode 100644 .github/docs/actions/android/setup-environment.md create mode 100644 .github/docs/actions/index.md create mode 100644 .github/docs/actions/ios/export-secrets.md create mode 100644 .github/docs/actions/ios/fastlane-beta.md create mode 100644 .github/docs/actions/ios/fastlane-release.md create mode 100644 .github/docs/actions/ios/fastlane-test.md create mode 100644 .github/docs/actions/ios/index.md create mode 100644 .github/docs/actions/ios/kmp-build.md create mode 100644 .github/docs/actions/utility/detect-changes-changelog.md create mode 100644 .github/docs/actions/utility/index.md create mode 100644 .github/docs/actions/utility/jira-transition-tickets.md create mode 100644 .github/docs/actions/utility/kmp-detect-changes.md create mode 100755 .github/docs/assets/fonts/Roobert-Bold.woff2 create mode 100755 .github/docs/assets/fonts/Roobert-Regular.woff2 create mode 100755 .github/docs/assets/fonts/Roobert-SemiBold.woff2 create mode 100644 .github/docs/assets/images/favicon.png create mode 100644 .github/docs/index.md create mode 100644 .github/docs/overrides/.icons/futured/logo.svg create mode 100644 .github/docs/stylesheets/extra.css create mode 100644 .github/docs/workflows/android/cloud-check.md create mode 100644 .github/docs/workflows/android/cloud-generate-baseline-profiles.md create mode 100644 .github/docs/workflows/android/cloud-nightly-build.md create mode 100644 .github/docs/workflows/android/cloud-release-firebase.md create mode 100644 .github/docs/workflows/android/cloud-release-googleplay.md create mode 100644 .github/docs/workflows/android/index.md create mode 100644 .github/docs/workflows/index.md create mode 100644 .github/docs/workflows/ios-kmp/index.md create mode 100644 .github/docs/workflows/ios-kmp/selfhosted-build.md create mode 100644 .github/docs/workflows/ios-kmp/selfhosted-release.md create mode 100644 .github/docs/workflows/ios-kmp/selfhosted-test.md create mode 100644 .github/docs/workflows/ios/index.md create mode 100644 .github/docs/workflows/ios/selfhosted-build.md create mode 100644 .github/docs/workflows/ios/selfhosted-nightly-build.md create mode 100644 .github/docs/workflows/ios/selfhosted-on-demand-build.md create mode 100644 .github/docs/workflows/ios/selfhosted-release.md create mode 100644 .github/docs/workflows/ios/selfhosted-test.md create mode 100644 .github/docs/workflows/kmp/cloud-detect-changes.md create mode 100644 .github/docs/workflows/kmp/combined-nightly-build.md create mode 100644 .github/docs/workflows/kmp/index.md create mode 100644 .github/docs/workflows/universal/cloud-backup.md create mode 100644 .github/docs/workflows/universal/index.md create mode 100644 .github/docs/workflows/universal/selfhosted-backup.md create mode 100644 .github/docs/workflows/universal/workflows-lint.md create mode 100644 .github/mkdocs.yml create mode 100644 .github/requirements-docs.txt create mode 100644 .github/scripts/config.py create mode 100644 .github/scripts/enrichers/__init__.py create mode 100644 .github/scripts/enrichers/ai_enricher.py create mode 100644 .github/scripts/enrichers/base.py create mode 100644 .github/scripts/enrichers/readme_enricher.py create mode 100644 .github/scripts/generate-docs.py create mode 100644 .github/scripts/parsers/__init__.py create mode 100644 .github/scripts/parsers/action_parser.py create mode 100644 .github/scripts/parsers/types.py create mode 100644 .github/scripts/parsers/workflow_parser.py create mode 100644 .github/scripts/renderers/__init__.py create mode 100644 .github/scripts/renderers/markdown_renderer.py create mode 100644 .github/scripts/templates/action.md.j2 create mode 100644 .github/scripts/templates/index.md.j2 create mode 100644 .github/scripts/templates/workflow.md.j2 create mode 100644 .github/workflows/deploy-docs.yml diff --git a/.github/docs/actions/android/build-firebase.md b/.github/docs/actions/android/build-firebase.md new file mode 100644 index 0000000..73bcf10 --- /dev/null +++ b/.github/docs/actions/android/build-firebase.md @@ -0,0 +1,36 @@ + + +# Build Firebase + +**Source:** [`actions/android-build-firebase/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/android-build-firebase/action.yml) + +Builds and uploads app to Firebase App Distribution. + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/android-build-firebase@main + with: + test_gradle_task: '...' + package_gradle_task: '...' + upload_gradle_task: '...' + app_distribution_groups: '...' + app_distribution_service_account: '...' +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `test_gradle_task` | `string` | Yes | — | A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest` | +| `package_gradle_task` | `string` | Yes | — | A Gradle task for packaging universal APK, eg. 'packageEnterpriseUniversalApk' | +| `upload_gradle_task` | `string` | Yes | — | A Gradle task for uploading APK, for example `appDistributionUploadEnterprise` | +| `app_distribution_groups` | `string` | Yes | — | Comma-separated list of Firebase App Distribution group IDs | +| `app_distribution_service_account` | `string` | Yes | — | JSON key of service account with permissions to upload build to Firebase App Distribution | +| `version_name` | `string` | No | — | Version name. Example: '1.X.X-snapshot' | +| `build_number_offset` | `string` | No | `0` | Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers. | +| `release_notes` | `string` | No | `${{ github.event.head_commit.message }}` | Release notes for this build | +| `kmp_flavor` | `string` | No | `test` | KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects | +| `secret_properties_file` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | +| `secret_properties` | `string` | No | — | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | + diff --git a/.github/docs/actions/android/build-googleplay.md b/.github/docs/actions/android/build-googleplay.md new file mode 100644 index 0000000..95a71ac --- /dev/null +++ b/.github/docs/actions/android/build-googleplay.md @@ -0,0 +1,41 @@ + + +# Build Google Play + +**Source:** [`actions/android-build-googlePlay/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/android-build-googlePlay/action.yml) + +Builds and uploads app to Google Play. + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/android-build-googlePlay@main + with: + bundle_gradle_task: '...' + version_name: '...' + signing_keystore_password: '...' + signing_key_alias: '...' + signing_key_password: '...' + google_play_application_id: '...' + google_play_whatsnew_dir: '...' + google_play_publish_service_account: '...' +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `bundle_gradle_task` | `string` | Yes | — | A Gradle task for assembling app bundle, for example `bundleRelease` | +| `version_name` | `string` | Yes | — | Version name. Example: '1.0.0' | +| `signing_keystore_password` | `string` | Yes | — | Password to provided keystore | +| `signing_key_alias` | `string` | Yes | — | Alias of the signing key in the provided keystore | +| `signing_key_password` | `string` | Yes | — | Password to the key in the provided keystore | +| `google_play_application_id` | `string` | Yes | — | Google Play applicationId | +| `google_play_whatsnew_dir` | `string` | Yes | — | Path to directory with changelog files according to documentation in https://github.com/r0adkll/upload-google-play | +| `google_play_publish_service_account` | `string` | Yes | — | JSON key of service account with permissions to upload build to Google Play | +| `changes_not_sent_for_review` | `string` | No | `False` | A changesNotSentForReview Google Play flag. Enable when last google review failed, disable when last review was successful. | +| `build_number_offset` | `string` | No | `0` | Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers. | +| `kmp_flavor` | `string` | No | `prod` | KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects | +| `secret_properties_file` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | +| `secret_properties` | `string` | No | — | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | + diff --git a/.github/docs/actions/android/check.md b/.github/docs/actions/android/check.md new file mode 100644 index 0000000..cab7809 --- /dev/null +++ b/.github/docs/actions/android/check.md @@ -0,0 +1,25 @@ + + +# Android Check + +**Source:** [`actions/android-check/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/android-check/action.yml) + +Runs lint checks and unit tests. + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/android-check@main + with: + lint_gradle_task: '...' + test_gradle_task: '...' +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `lint_gradle_task` | `string` | Yes | — | A Gradle task(s) for executing lint check, for example `lintCheck lintRelease` | +| `test_gradle_task` | `string` | Yes | — | A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest` | +| `github_token_danger` | `string` | No | — | GitHub token for Danger. Must have permissions to read and write issues and pull requests. | + diff --git a/.github/docs/actions/android/generate-baseline-profiles.md b/.github/docs/actions/android/generate-baseline-profiles.md new file mode 100644 index 0000000..c6f3898 --- /dev/null +++ b/.github/docs/actions/android/generate-baseline-profiles.md @@ -0,0 +1,30 @@ + + +# Generate Baseline Profiles + +**Source:** [`actions/android-generate-baseline-profiles/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/android-generate-baseline-profiles/action.yml) + +Generates baseline profiles and creates a PR with the changes + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/android-generate-baseline-profiles@main + with: + generate_gradle_task: '...' + signing_keystore_password: '...' + signing_key_alias: '...' + signing_key_password: '...' +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `generate_gradle_task` | `string` | Yes | — | A Gradle task for generating baseline profiles, for example `generateBaselineProfile` | +| `signing_keystore_password` | `string` | Yes | — | Password to provided keystore | +| `signing_key_alias` | `string` | Yes | — | Alias of the signing key in the provided keystore | +| `signing_key_password` | `string` | Yes | — | Password to the key in the provided keystore | +| `secret_properties_file` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | +| `secret_properties` | `string` | No | — | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | + diff --git a/.github/docs/actions/android/index.md b/.github/docs/actions/android/index.md new file mode 100644 index 0000000..361e8e4 --- /dev/null +++ b/.github/docs/actions/android/index.md @@ -0,0 +1,14 @@ + + +# Android Actions + +Composite GitHub Actions for Android projects. + +| Name | Description | +|------|-------------| +| [Setup Environment](setup-environment.md) | Sets up Java, Gradle and Ruby and other preconditions for CI runs at Futured Android workflows. | +| [Android Check](check.md) | Runs lint checks and unit tests. | +| [Build Firebase](build-firebase.md) | Builds and uploads app to Firebase App Distribution. | +| [Build Google Play](build-googleplay.md) | Builds and uploads app to Google Play. | +| [Generate Baseline Profiles](generate-baseline-profiles.md) | Generates baseline profiles and creates a PR with the changes | + diff --git a/.github/docs/actions/android/setup-environment.md b/.github/docs/actions/android/setup-environment.md new file mode 100644 index 0000000..056c815 --- /dev/null +++ b/.github/docs/actions/android/setup-environment.md @@ -0,0 +1,26 @@ + + +# Setup Environment + +**Source:** [`actions/android-setup-environment/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/android-setup-environment/action.yml) + +Sets up Java, Gradle and Ruby and other preconditions for CI runs at Futured Android workflows. + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/android-setup-environment@main +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `java` | `string` | No | `true` | Whether to set up Java | +| `java_version` | `string` | No | `17` | Java version to use, eg. '17'. | +| `java_distribution` | `string` | No | `zulu` | Java distribution to use, eg 'zulu'. | +| `ruby` | `string` | No | `true` | Whether to set up Ruby | +| `ruby_version` | `string` | No | `3.4` | Ruby version. | +| `gradle` | `string` | No | `true` | Whether to set up Gradle | +| `gradle_cache_encryption_key` | `string` | No | — | Configuration cache encryption key. Leave empty if you don't need cache. | + diff --git a/.github/docs/actions/index.md b/.github/docs/actions/index.md new file mode 100644 index 0000000..d0e7a72 --- /dev/null +++ b/.github/docs/actions/index.md @@ -0,0 +1,12 @@ + + +# Actions + +All composite GitHub Actions organized by platform. + +| Name | Description | +|------|-------------| +| [Android Actions](android/index.md) | 5 action(s) | +| [iOS Actions](ios/index.md) | 5 action(s) | +| [Utility Actions](utility/index.md) | 3 action(s) | + diff --git a/.github/docs/actions/ios/export-secrets.md b/.github/docs/actions/ios/export-secrets.md new file mode 100644 index 0000000..3dbc8f6 --- /dev/null +++ b/.github/docs/actions/ios/export-secrets.md @@ -0,0 +1,24 @@ + + +# Export Secrets + +**Source:** [`actions/ios-export-secrets/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/ios-export-secrets/action.yml) + +Encodes secret values to Base64 and writes them into the specified .xcconfig file in KEY = VALUE format. + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/ios-export-secrets@main + with: + XCCONFIG_PATH: '...' +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `XCCONFIG_PATH` | `string` | Yes | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | +| `SECRET_PROPERTIES` | `string` | No | — | Secrets in the format KEY = VALUE (one per line). | +| `REQUIRED_KEYS` | `string` | No | — | Comma-separated list of required keys. | + diff --git a/.github/docs/actions/ios/fastlane-beta.md b/.github/docs/actions/ios/fastlane-beta.md new file mode 100644 index 0000000..fe25427 --- /dev/null +++ b/.github/docs/actions/ios/fastlane-beta.md @@ -0,0 +1,32 @@ + + +# Fastlane Beta + +**Source:** [`actions/ios-fastlane-beta/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/ios-fastlane-beta/action.yml) + +Runs Fastlane beta + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/ios-fastlane-beta@main + with: + match_password: '...' + app_store_connect_api_key_key: '...' + app_store_connect_api_key_key_id: '...' + app_store_connect_api_key_issuer_id: '...' +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `match_password` | `string` | Yes | — | Match password | +| `testflight_changelog` | `string` | No | — | Testflight changelog | +| `app_store_connect_api_key_key` | `string` | Yes | — | App Store Connect API Key | +| `app_store_connect_api_key_key_id` | `string` | Yes | — | App Store Connect API Key ID | +| `app_store_connect_api_key_issuer_id` | `string` | Yes | — | App Store Connect API Key Issuer ID | +| `custom_values` | `string` | No | — | Custom values | +| `ios_root_path` | `string` | No | — | Path to iOS project root directory containing Gemfile. If not specified, uses current directory. | +| `custom_build_path` | `string` | No | — | Path to directory containing Fastfile. If not specified, uses ios_root_path or current directory. | + diff --git a/.github/docs/actions/ios/fastlane-release.md b/.github/docs/actions/ios/fastlane-release.md new file mode 100644 index 0000000..4dcb963 --- /dev/null +++ b/.github/docs/actions/ios/fastlane-release.md @@ -0,0 +1,32 @@ + + +# Fastlane Release + +**Source:** [`actions/ios-fastlane-release/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/ios-fastlane-release/action.yml) + +Runs Fastlane release + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/ios-fastlane-release@main + with: + match_password: '...' + app_store_connect_api_key_key: '...' + app_store_connect_api_key_key_id: '...' + app_store_connect_api_key_issuer_id: '...' +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `match_password` | `string` | Yes | — | Match password | +| `version_number` | `string` | No | — | Version number | +| `app_store_connect_api_key_key` | `string` | Yes | — | App Store Connect API Key | +| `app_store_connect_api_key_key_id` | `string` | Yes | — | App Store Connect API Key ID | +| `app_store_connect_api_key_issuer_id` | `string` | Yes | — | App Store Connect API Key Issuer ID | +| `custom_values` | `string` | No | — | Custom values | +| `ios_root_path` | `string` | No | — | Path to iOS project root directory containing Gemfile. If not specified, uses current directory. | +| `custom_build_path` | `string` | No | — | Path to directory containing Fastfile. If not specified, uses ios_root_path or current directory. | + diff --git a/.github/docs/actions/ios/fastlane-test.md b/.github/docs/actions/ios/fastlane-test.md new file mode 100644 index 0000000..0c20f2e --- /dev/null +++ b/.github/docs/actions/ios/fastlane-test.md @@ -0,0 +1,23 @@ + + +# Fastlane Test + +**Source:** [`actions/ios-fastlane-test/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/ios-fastlane-test/action.yml) + +Runs Fastlane test + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/ios-fastlane-test@main +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `github_token` | `string` | No | — | GitHub token | +| `custom_values` | `string` | No | — | Custom values | +| `ios_root_path` | `string` | No | — | Path to iOS project root directory containing Gemfile. If not specified, uses current directory. | +| `custom_build_path` | `string` | No | — | Path to directory containing Fastfile. If not specified, uses ios_root_path or current directory. | + diff --git a/.github/docs/actions/ios/index.md b/.github/docs/actions/ios/index.md new file mode 100644 index 0000000..15c1bc9 --- /dev/null +++ b/.github/docs/actions/ios/index.md @@ -0,0 +1,14 @@ + + +# iOS Actions + +Composite GitHub Actions for iOS projects. + +| Name | Description | +|------|-------------| +| [Export Secrets](export-secrets.md) | Encodes secret values to Base64 and writes them into the specified .xcconfig file in KEY = VALUE format. | +| [Fastlane Test](fastlane-test.md) | Runs Fastlane test | +| [Fastlane Beta](fastlane-beta.md) | Runs Fastlane beta | +| [Fastlane Release](fastlane-release.md) | Runs Fastlane release | +| [KMP Build](kmp-build.md) | Builds iOS app (optionally with KMP framework) and uploads to TestFlight | + diff --git a/.github/docs/actions/ios/kmp-build.md b/.github/docs/actions/ios/kmp-build.md new file mode 100644 index 0000000..e02ba58 --- /dev/null +++ b/.github/docs/actions/ios/kmp-build.md @@ -0,0 +1,37 @@ + + +# KMP Build + +**Source:** [`actions/ios-kmp-build/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/ios-kmp-build/action.yml) + +Builds iOS app (optionally with KMP framework) and uploads to TestFlight + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/ios-kmp-build@main + with: + match_password: '...' + app_store_connect_api_key_key: '...' + app_store_connect_api_key_key_id: '...' + app_store_connect_api_key_issuer_id: '...' +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `match_password` | `string` | Yes | — | Password for decrypting of certificates and provisioning profiles. | +| `app_store_connect_api_key_key` | `string` | Yes | — | Private App Store Connect API key for submitting build to App Store. | +| `app_store_connect_api_key_key_id` | `string` | Yes | — | Private App Store Connect API key for submitting build to App Store. | +| `app_store_connect_api_key_issuer_id` | `string` | Yes | — | Private App Store Connect API issuer key for submitting build to App Store. | +| `secret_xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | +| `secret_properties` | `string` | No | — | Secrets in the format KEY = VALUE (one per line). | +| `secret_required_keys` | `string` | No | — | Comma-separated list of required secret keys. | +| `kmp_swift_package_integration` | `string` | No | — | Whether KMP is integrated in Xcode project as a Swift Package | +| `kmp_swift_package_path` | `string` | No | — | If `kmp_swift_package_integration` is 'true', then specifies a location of local Swift Package with Makefile. Example: 'iosApp/shared/KMP` | +| `kmp_swift_package_flavor` | `string` | No | — | If `kmp_swift_package_integration`, specifies build flavor of KMP Package | +| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | +| `testflight_changelog` | `string` | No | — | Will be used as TestFlight changelog | +| `ios_custom_build_path` | `string` | No | — | Path to the directory containing Fastfile. If not specified, uses iosApp. Example: iosApp/appA | + diff --git a/.github/docs/actions/utility/detect-changes-changelog.md b/.github/docs/actions/utility/detect-changes-changelog.md new file mode 100644 index 0000000..348a8bc --- /dev/null +++ b/.github/docs/actions/utility/detect-changes-changelog.md @@ -0,0 +1,148 @@ + + +# Detect Changes & Changelog + +**Source:** [`actions/universal-detect-changes-and-generate-changelog/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/universal-detect-changes-and-generate-changelog/action.yml) + +Detects changes since the last built commit and generates a changelog. + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/universal-detect-changes-and-generate-changelog@main +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `checkout_depth` | `number` | No | `100` | The depth of the git history to fetch. Default is 100. | +| `debug` | `boolean` | No | `False` | Enable debug mode. Default is false. | +| `fallback_lookback` | `string` | No | `24 hours` | The amount of time to look back for merge commits when no previous build commit is found (e.g., "24 hours", "7 days", "2 weeks"). Default is "24 hours". | +| `cache_key_prefix` | `string` | No | — | Custom prefix for cache keys. If not provided, will use latest_builded_commit-. If provided, format will be {prefix}-latest_builded_commit- | +| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files during checkout. Default is false. | +| `exclude_source_branches` | `string` | No | `(main|develop|master)` | Exclude merged commits of given branches. Regex pattern (ERE). Example: "(release.*|hotfix.*)" | + +## Outputs + +| Name | Description | +|------|-------------| +| `skip_build` | Indicates if the build should be skipped | +| `changelog` | The generated changelog formatted as a string | +| `merged_branches` | List of merged branch names | +| `cache_key` | Cache key to store latest built commit for this branch | + +## Additional Details + +## Features + +- ✅ **Nested Merge Detection**: Detects ALL merged branches including nested merges (e.g., B→A→develop reports both A and B) +- ✅ **Smart Filtering**: Automatically filters out reverse merges (main→feature) used for conflict resolution +- ✅ **Modular Design**: Split into separate bash scripts for better maintainability +- ✅ **Customizable Cache Keys**: Support for custom cache key prefixes (format: `latest_builded_commit-` or `{prefix}-latest_builded_commit-`) +- ✅ **GitHub-Native**: Leverages GitHub's built-in branch handling (no manual sanitization) +- ✅ **Comprehensive Testing**: Unit tests for all bash scripts +- ✅ **Debug Support**: Detailed debug output when enabled + +## Nested Merge Detection + +The action detects **all merged branches** including nested merges where one feature branch merges into another before merging to main. + +### How It Works + +**Example:** Branch B merges into branch A, then A merges into develop +- **Output:** Both `feature-A` and `feature-B` are detected +- **Filtering:** Reverse merges (e.g., `develop→feature-A` for conflict resolution) are automatically excluded + +### Implementation Details + +- **Branch Names**: Uses `git log --merges` (without `--first-parent`) to see all merge commits +- **Changelog Messages**: Uses `git log --merges --first-parent` to follow only main branch history +- **Filtering**: Excludes source branches via `grep -v "Merge branch '(EXCLUDE_SOURCE_BRANCHES)' into"` + +### Performance Considerations + +Removing `--first-parent` for branch detection means git traverses more of the commit graph: +- **Impact**: Minimal for typical workflows (10-100 commits between builds) +- **Large histories**: May add 1-2 seconds for repos with 1000+ commits in the range +- **Recommendation**: Use `checkout_depth` to limit git history fetch depth if needed + +The performance tradeoff is generally acceptable given the improved accuracy in branch detection. + +## Scripts + +### `cache-keys.sh` +Handles cache key calculation. + +**Environment Variables:** +- `CACHE_KEY_PREFIX`: Custom cache key prefix +- `DEBUG`: Debug mode flag + +**Outputs:** +- `cache_key_prefix`: Generated cache key prefix (format: `latest_builded_commit-` or `{prefix}-latest_builded_commit-`) + +### `determine-range.sh` +Determines commit range and skip build logic. + +**Environment Variables:** +- `DEBUG`: Debug mode flag +- `FALLBACK_LOOKBACK`: Fallback time window + +**Outputs:** +- `build_should_skip`: Whether to skip the build +- `from_commit`: Starting commit for changelog +- `to_commit`: Ending commit for changelog + +### `generate-changelog.sh` +Generates formatted changelog and branch names with nested merge detection. + +**Environment Variables:** +- `FROM_COMMIT`: Starting commit +- `TO_COMMIT`: Ending commit +- `EXCLUDE_SOURCE_BRANCHES`: Regex pattern for excluding source branches (default: `(main|develop|master)`) +- `DEBUG`: Debug mode flag + +**Outputs:** +- `changelog_string`: Formatted changelog (from main branch history only) +- `merged_branches`: List of all merged branches (includes nested merges) + +## Testing + +The action includes comprehensive unit tests using BATS (Bash Automated Testing System) and automated CI testing. + +### Running Tests + +```bash +# Run all tests +./test/run_tests.sh + +# Run specific test file +bats test/test_cache-keys.bats +bats test/test_determine-range.bats +bats test/test_generate-changelog.bats +bats test/test_merged-branches.bats +``` + +### CI Testing + +Tests run automatically on pull requests when relevant files change. The CI workflow includes: +- Unit tests for all bash scripts +- YAML syntax validation +- Concurrency cancellation (new commits cancel old tests) + +### Test Coverage + +- ✅ Cache key generation with custom prefixes +- ✅ Branch name detection and fallback logic +- ✅ Commit range determination logic +- ✅ Skip build decision making +- ✅ Changelog generation and formatting +- ✅ **Nested merge detection** (B→A→develop, C→B→A→develop) +- ✅ **Reverse merge filtering** (conflict resolution exclusion) +- ✅ **Custom target branch patterns** +- ✅ Error handling and edge cases +- ✅ Debug output functionality +- ✅ Git command failure scenarios +- ✅ Empty input handling +- ✅ Special characters and edge cases + diff --git a/.github/docs/actions/utility/index.md b/.github/docs/actions/utility/index.md new file mode 100644 index 0000000..8725fd6 --- /dev/null +++ b/.github/docs/actions/utility/index.md @@ -0,0 +1,12 @@ + + +# Utility Actions + +Composite GitHub Actions for Utility projects. + +| Name | Description | +|------|-------------| +| [KMP Detect Changes](kmp-detect-changes.md) | Detects changes in KMP project to determine which platform-specific workflows should run | +| [Detect Changes & Changelog](detect-changes-changelog.md) | Detects changes since the last built commit and generates a changelog. | +| [JIRA Transition Tickets](jira-transition-tickets.md) | Finds and transitions JIRA tickets based on branch names. | + diff --git a/.github/docs/actions/utility/jira-transition-tickets.md b/.github/docs/actions/utility/jira-transition-tickets.md new file mode 100644 index 0000000..8705235 --- /dev/null +++ b/.github/docs/actions/utility/jira-transition-tickets.md @@ -0,0 +1,120 @@ + + +# JIRA Transition Tickets + +**Source:** [`actions/jira-transition-tickets/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/jira-transition-tickets/action.yml) + +Finds and transitions JIRA tickets based on branch names. + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/jira-transition-tickets@main + with: + jira_context: '...' + transition: '...' + merged_branches: '...' +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `jira_context` | `string` | Yes | — | A base64-encoded string of Jira context object. See README for required structure. | +| `transition` | `string` | Yes | — | The name of the transition to transition the JIRA tickets. | +| `merged_branches` | `string` | Yes | — | A comma-separated string of merged branch names. | + +## Additional Details + +### `jira_context` (required) + +A base64-encoded JSON string containing JIRA authentication credentials and configuration. + +**Structure:** +```json +{ + "cloud_id": "your-cloud-id", + "user_email": "your-bot@serviceaccount.atlassian.com", + "api_token": "YourJiraApiToken" +} +``` + +**How to obtain Cloud ID:** + +Navigate to [https://.atlassian.net/_edge/tenant_info](https://.atlassian.net/_edge/tenant_info) + +**How to encode:** +```bash +echo -n '{"cloud_id":"your-cloud-id","user_email":"bot@example.com","api_token":"token"}' | base64 +``` + +**GitHub Secrets:** +Store the base64-encoded string in a GitHub secret (e.g., `JIRA_CONTEXT`) for secure usage. + +### `transition` (required) + +The name of the JIRA transition to execute. This must match the exact transition name in your JIRA workflow. + +**Examples:** `"Done"`, `"In QA"`, `"Ready for Testing"`, `"Closed"` + +### `merged_branches` (required) + +A comma-separated string of branch names from which to extract JIRA ticket keys. + +The action extracts keys matching the pattern `[A-Z]+-[0-9]+` from each branch name. + +**Example:** `"feature/ABC-123-login,bugfix/XYZ-456-fix-crash"` + +## How It Works + +1. **Extract JIRA Keys:** Parses branch names to extract ticket keys (e.g., `ABC-123`) +2. **Get Available Transitions:** For each issue key, fetches available transitions from JIRA API +3. **Find Target Transition:** Matches the target status name to find the corresponding transition ID +4. **Perform Transition:** Executes the transition for each issue to move it to the target status + +## Usage Examples + +### Example 1: Transition tickets from merged branches + +```yaml +- name: Transition JIRA tickets + uses: futuredapp/.github/.github/actions/jira-transition-tickets@main + with: + jira_context: ${{ secrets.JIRA_CONTEXT }} + transition: "Ready for Testing" + merged_branches: "feature/PROJ-123-new-feature,bugfix/PROJ-456-bug-fix" +``` + +### Example 2: Transition tickets from dynamic branch list + +```yaml +- name: Transition tickets + uses: futuredapp/.github/.github/actions/jira-transition-tickets@main + with: + jira_context: ${{ secrets.JIRA_CONTEXT }} + transition: "Ready for Testing" + merged_branches: ${{ steps.get_branches.outputs.branches }} +``` + +### Example 3: In a reusable workflow + +```yaml +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build app + run: ./build.sh + + - name: Transition JIRA tickets on success + if: success() + uses: futuredapp/.github/.github/actions/jira-transition-tickets@main + with: + jira_context: ${{ secrets.JIRA_CONTEXT }} + transition: "Ready for Testing" + merged_branches: ${{ github.head_ref }} +``` + diff --git a/.github/docs/actions/utility/kmp-detect-changes.md b/.github/docs/actions/utility/kmp-detect-changes.md new file mode 100644 index 0000000..72dce69 --- /dev/null +++ b/.github/docs/actions/utility/kmp-detect-changes.md @@ -0,0 +1,27 @@ + + +# KMP Detect Changes + +**Source:** [`actions/kmp-detect-changes/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/kmp-detect-changes/action.yml) + +Detects changes in KMP project to determine which platform-specific workflows should run + +## Usage + +```yaml +- uses: futuredapp/.github/.github/actions/kmp-detect-changes@main +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | + +## Outputs + +| Name | Description | +|------|-------------| +| `iosFiles` | Whether files affecting iOS build changed (all files except those in androidApp/) | +| `androidFiles` | Whether files affecting Android build changed (all files except those in iosApp/) | + diff --git a/.github/docs/assets/fonts/Roobert-Bold.woff2 b/.github/docs/assets/fonts/Roobert-Bold.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..bb15dcf41c189f3d234edbf9e606018c1ce4b7f8 GIT binary patch literal 79616 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⪙lqFgxn!`FPo`OX|lB2HX2 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

5S+duZasb2(Iz$|FjYR(3TvBn-ZF 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 literal 0 HcmV?d00001 diff --git a/.github/docs/assets/fonts/Roobert-Regular.woff2 b/.github/docs/assets/fonts/Roobert-Regular.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..5ee34c738f4faa457fcd76db50de4ec405f82536 GIT binary patch literal 77000 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 ShK)!lXZLmFaP9-|Z+#pwNCaL0 literal 0 HcmV?d00001 diff --git a/.github/docs/assets/fonts/Roobert-SemiBold.woff2 b/.github/docs/assets/fonts/Roobert-SemiBold.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..00c93fc1f68313500ff1f067c4ea14d925e66729 GIT binary patch literal 78408 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%0D$}{I^!HXg5nA{~0R=X0Y;&t&6YMI1QxTt}Y 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 WVfGqfP9|!aEcWar|M~KOo)Z9vm>>KA literal 0 HcmV?d00001 diff --git a/.github/docs/assets/images/favicon.png b/.github/docs/assets/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..54d52cf167464ae4b7f610e2faea172f68696a3c GIT binary patch literal 374 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&*$4Q9xB}__|NlEVIo-T@bLrBh zeSLioA3i*H?wqBi<(oHe4jnqQaN$B9A0Ib2x9!`v&zLd8z`$VLx^-1mRngJW3JMB5 zll}RDMtt&gaSW-L^LDx?-ys75md%p|oNoO4AGYp?ne5jiPOGXKR;$S0$&&)A1cM1T zcX8aF^ECdJL~v^T?dLy!udvrtcz2$2$C1C9j9vE14xiLnGV1CNgnquw{BF^G*$u1e ziVx`h-pjc7-F7yakJj>wciQ|Y{8fDC>0Mn$76Ata#(JR*SNUcv|JVNfC!^}c!WmyJ zdHWwPSuM5b-x7vIndK}Rj}4R^q?U6s-m>_{-tqX3+=oklzA;>V&sVX!uDIddceXnv z`%NEo-2W+2+tob1l*8~B(<=T+uO{~}FmWg}G`wPbnmpy}b5P)cKtogwBf|`?S{sXq PX&}C*tDnm{r-UW|6>6eN literal 0 HcmV?d00001 diff --git a/.github/docs/index.md b/.github/docs/index.md new file mode 100644 index 0000000..2059e9f --- /dev/null +++ b/.github/docs/index.md @@ -0,0 +1,60 @@ +# Futured CI/CD Workflows + +Reusable GitHub Actions workflows and composite actions for **iOS**, **Android**, and **Kotlin Multiplatform** projects at Futured. + +--- + +## What's Inside + +

+ +--- + +## Quick Links + +| Platform | Test | Build | Release | +|----------|------|-------|---------| +| **iOS** | [selfhosted-test](workflows/ios/selfhosted-test.md) | [selfhosted-nightly-build](workflows/ios/selfhosted-nightly-build.md) | [selfhosted-release](workflows/ios/selfhosted-release.md) | +| **iOS + KMP** | [selfhosted-test](workflows/ios-kmp/selfhosted-test.md) | [selfhosted-build](workflows/ios-kmp/selfhosted-build.md) | [selfhosted-release](workflows/ios-kmp/selfhosted-release.md) | +| **Android** | [cloud-check](workflows/android/cloud-check.md) | [cloud-nightly-build](workflows/android/cloud-nightly-build.md) | [Firebase](workflows/android/cloud-release-firebase.md) / [Google Play](workflows/android/cloud-release-googleplay.md) | +| **KMP** | — | [combined-nightly-build](workflows/kmp/combined-nightly-build.md) | — | + +--- + +## Repository + +[:fontawesome-brands-github: futuredapp/.github](https://github.com/futuredapp/.github){ .md-button } diff --git a/.github/docs/overrides/.icons/futured/logo.svg b/.github/docs/overrides/.icons/futured/logo.svg new file mode 100644 index 0000000..aedd25d --- /dev/null +++ b/.github/docs/overrides/.icons/futured/logo.svg @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/.github/docs/stylesheets/extra.css b/.github/docs/stylesheets/extra.css new file mode 100644 index 0000000..5fde3c6 --- /dev/null +++ b/.github/docs/stylesheets/extra.css @@ -0,0 +1,176 @@ +@font-face { + font-family: 'Roobert'; + src: url('../assets/fonts/Roobert-Regular.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Roobert'; + src: url('../assets/fonts/Roobert-SemiBold.woff2') format('woff2'); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Roobert'; + src: url('../assets/fonts/Roobert-Bold.woff2') format('woff2'); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +:root { + --md-text-font: "Roobert" +} + +[data-md-color-scheme="default"] { + --ftrd-background: #FFFFFF; + --ftrd-background-variant: #0000001A; + --ftrd-background-variant2: #0000000D; + --ftrd-foreground: #222222; + --ftrd-foreground-variant: #00000099; + --ftrd-foreground-variant2: #00000066; + --ftrd-foreground-variant3: #22222299; + --ftrd-border: #E7E8E9; + --ftrd-brand: #FF5F00; + + --md-primary-fg-color: var(--ftrd-brand); + --md-primary-bg-color: var(--ftrd-foreground); + + --md-default-fg-color: var(--ftrd-foreground); + --md-default-bg-color: var(--ftrd-background); + + --md-accent-fg-color: var(--ftrd-brand); + --md-typeset-a-color: var(--ftrd-brand) !important; +} + +[data-md-color-scheme="slate"] { + --ftrd-background: #1B1B1B; + --ftrd-background-variant: #FFFFFF1A; + --ftrd-background-variant2: #FFFFFF0D; + --ftrd-foreground: #E7E8E9; + --ftrd-foreground-variant: #C2C5C8; + --ftrd-foreground-variant2: #C2C5C8; + --ftrd-foreground-variant3: #C2C5C8; + --ftrd-border: #292929; + --ftrd-brand: #FF5F00; + + --md-primary-fg-color: var(--ftrd-brand); + --md-primary-bg-color: var(--ftrd-foreground); + + --md-default-fg-color: var(--ftrd-foreground); + --md-default-bg-color: var(--ftrd-background); + + --md-accent-fg-color: var(--ftrd-brand); + + --md-code-bg-color: var(--ftrd-background-variant); + --md-typeset-a-color: var(--ftrd-brand) !important; +} + +/* Header background */ +.md-header { + background-color: var(--ftrd-background); +} + +/* Header title margin */ +.md-header__title { + margin-left: 0.2rem !important; +} + +/* Nav title background in drawer */ +.md-nav--primary .md-nav__title[for=__drawer] { + background-color: var(--ftrd-background); +} + +/* Search widget - icon */ +.md-search__icon[for=__search] { + color: var(--ftrd-foreground-variant); +} + +/* Search widget - background */ +.md-search__form { + background-color: var(--ftrd-background-variant); +} + +/* Search widget - hover */ +.md-search__form:hover { + background-color: var(--ftrd-background-variant2); +} + +/* Search widget - input placeholder color */ +.md-search__input::placeholder { + color: var(--ftrd-foreground-variant); +} + +/* Palette button colors */ +.md-header__button[for="__palette_0"], +.md-header__button[for="__palette_1"], +.md-header__button[for="__palette_2"] { + color: var(--ftrd-foreground-variant); +} + +/* Tab background */ +.md-tabs { + background-color: var(--ftrd-background); + border-bottom: 1px solid var(--ftrd-border); +} + +/* Tab item color */ +.md-tabs__item { + color: var(--ftrd-foreground) !important; +} + +/* Tab item opacity, gets rid of transition animation, did not find a way to do transition to brand color */ +.md-tabs__link { + opacity: 1; +} + +/* Tab item color -- selected */ +.md-tabs__item--active { + color: var(--ftrd-brand) !important; +} + +/* Navigation - scrollbar color */ +.md-sidebar__scrollwrap:hover { + scrollbar-color: var(--ftrd-foreground-variant) #0000; +} + +/* Navigation - section color */ +.md-nav__item--section>.md-nav__link[for] { + color: var(--ftrd-foreground-variant); +} + +/* Typography - Page title */ +.md-typeset h1 { + color: var(--ftrd-foreground) !important; +} + +/* Footer - background*/ +.md-footer-meta { + background-color: var(--ftrd-background); + border-top: 1px solid var(--ftrd-border); +} + +/* Footer - Made with ❤️‍🔥 at Futured */ +.md-copyright__highlight { + color: var(--ftrd-foreground); +} + +/* Footer - Made with mkdocs */ +.md-copyright { + color: var(--ftrd-foreground-variant3); +} + +/* Footer - Made with mkdocs - link */ +.md-copyright a { + color: var(--ftrd-foreground-variant3) !important; + text-decoration: underline; +} + +/* Footer - social icons */ +.md-social a { + color: var(--ftrd-foreground) !important; +} \ No newline at end of file diff --git a/.github/docs/workflows/android/cloud-check.md b/.github/docs/workflows/android/cloud-check.md new file mode 100644 index 0000000..0bc943a --- /dev/null +++ b/.github/docs/workflows/android/cloud-check.md @@ -0,0 +1,44 @@ + + +# Android PR Check + +**Source:** [`workflows/android-cloud-check.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-check.yml) +**Runner:** `ubuntu-latest` + +*Android Pull Request Check* + +## Usage + +```yaml +jobs: + android-pr-check: + uses: futuredapp/.github/.github/workflows/android-cloud-check.yml@main + with: + LINT_GRADLE_TASKS: '...' + TEST_GRADLE_TASKS: '...' +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `LINT_GRADLE_TASKS` | `string` | Yes | — | A Gradle task(s) for executing lint check, for example `lintCheck lintRelease` | +| `TEST_GRADLE_TASKS` | `string` | Yes | — | A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest` | +| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | +| `TIMEOUT_MINUTES` | `number` | No | `30` | Job timeout in minutes | +| `JAVA_VERSION` | `string` | No | `17` | Java version to use | +| `JAVA_DISTRIBUTION` | `string` | No | `zulu` | Java distribution to use | +| `GRADLE_OPTS` | `string` | No | — | Gradle options | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | +| `GITHUB_TOKEN_DANGER` | No | GitHub token for Danger. Must have permissions to read and write issues and pull requests. | + +## Internal Actions Used + +- [`Setup Environment`](../../actions/android/setup-environment.md) +- [`Android Check`](../../actions/android/check.md) + diff --git a/.github/docs/workflows/android/cloud-generate-baseline-profiles.md b/.github/docs/workflows/android/cloud-generate-baseline-profiles.md new file mode 100644 index 0000000..fc1cc0a --- /dev/null +++ b/.github/docs/workflows/android/cloud-generate-baseline-profiles.md @@ -0,0 +1,50 @@ + + +# Android Generate Baseline Profiles + +**Source:** [`workflows/android-cloud-generate-baseline-profiles.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-generate-baseline-profiles.yml) +**Runner:** `ubuntu-latest` + +*Generate baseline profiles* + +## Usage + +```yaml +jobs: + android-generate-baseline-profiles: + uses: futuredapp/.github/.github/workflows/android-cloud-generate-baseline-profiles.yml@main + with: + TASK_NAME: '...' + secrets: + SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} + SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} + SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `TASK_NAME` | `string` | Yes | — | A Gradle task for executing baseline profiles, for example `generateBaselineProfile` | +| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | +| `SECRET_PROPERTIES_FILE` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | +| `TIMEOUT_MINUTES` | `number` | No | `60` | Job timeout in minutes | +| `JAVA_VERSION` | `string` | No | `17` | Java version to use | +| `JAVA_DISTRIBUTION` | `string` | No | `zulu` | Java distribution to use | +| `GRADLE_OPTS` | `string` | No | — | Gradle options | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `SIGNING_KEYSTORE_PASSWORD` | Yes | Password to provided keystore | +| `SIGNING_KEY_ALIAS` | Yes | Alias of the signing key in the provided keystore | +| `SIGNING_KEY_PASSWORD` | Yes | Password to the key in the provided keystore | +| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | +| `SECRET_PROPERTIES` | No | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | + +## Internal Actions Used + +- [`Setup Environment`](../../actions/android/setup-environment.md) +- [`Generate Baseline Profiles`](../../actions/android/generate-baseline-profiles.md) + diff --git a/.github/docs/workflows/android/cloud-nightly-build.md b/.github/docs/workflows/android/cloud-nightly-build.md new file mode 100644 index 0000000..a64f941 --- /dev/null +++ b/.github/docs/workflows/android/cloud-nightly-build.md @@ -0,0 +1,62 @@ + + +# Android Nightly Build + +**Source:** [`workflows/android-cloud-nightly-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-nightly-build.yml) +**Runner:** `ubuntu-latest` + +*Android nightly build* + +## Usage + +```yaml +jobs: + android-nightly-build: + uses: futuredapp/.github/.github/workflows/android-cloud-nightly-build.yml@main + with: + TEST_GRADLE_TASKS: '...' + PACKAGE_GRADLE_TASK: '...' + UPLOAD_GRADLE_TASK: '...' + APP_DISTRIBUTION_GROUPS: '...' + secrets: + APP_DISTRIBUTION_SERVICE_ACCOUNT: ${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `TEST_GRADLE_TASKS` | `string` | Yes | — | A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest` | +| `PACKAGE_GRADLE_TASK` | `string` | Yes | — | A Gradle task for packaging universal APK, eg. 'packageEnterpriseUniversalApk' | +| `UPLOAD_GRADLE_TASK` | `string` | Yes | — | A Gradle task for uploading APK, for example `appDistributionUploadEnterprise` | +| `APP_DISTRIBUTION_GROUPS` | `string` | Yes | — | Comma-separated list of app distribution group IDs | +| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | +| `VERSION_NAME` | `string` | No | — | Version name. Example: '1.X.X-snapshot' | +| `BUILD_NUMBER_OFFSET` | `number` | No | `0` | Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers. | +| `KMP_FLAVOR` | `string` | No | `test` | KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects | +| `SECRET_PROPERTIES_FILE` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | +| `JAVA_VERSION` | `string` | No | `17` | Java version to use | +| `JAVA_DISTRIBUTION` | `string` | No | `zulu` | Java distribution to use | +| `GRADLE_OPTS` | `string` | No | — | Gradle options | +| `CHANGELOG_DEBUG` | `boolean` | No | `False` | Enable debug mode for changelog generation. Default is false. | +| `CHANGELOG_CHECKOUT_DEPTH` | `number` | No | `100` | The depth of the git history to fetch for changelog generation. Default is 100. | +| `CHANGELOG_FALLBACK_LOOKBACK` | `string` | No | `24 hours` | The amount of time to look back for merge commits when no previous build commit is found. Default is 24 hours. | +| `TIMEOUT_MINUTES` | `number` | No | `30` | Job timeout in minutes | +| `JIRA_TRANSITION` | `string` | No | `Testing` | Jira transition to use for transitioning related issues after build | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `APP_DISTRIBUTION_SERVICE_ACCOUNT` | Yes | JSON key of service account with permissions to upload build to Firebase App Distribution | +| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | +| `SECRET_PROPERTIES` | No | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | +| `JIRA_CONTEXT` | No | JIRA context for transitioning tickets. | + +## Internal Actions Used + +- [`Detect Changes & Changelog`](../../actions/utility/detect-changes-changelog.md) +- [`Setup Environment`](../../actions/android/setup-environment.md) +- [`Build Firebase`](../../actions/android/build-firebase.md) +- [`JIRA Transition Tickets`](../../actions/utility/jira-transition-tickets.md) + diff --git a/.github/docs/workflows/android/cloud-release-firebase.md b/.github/docs/workflows/android/cloud-release-firebase.md new file mode 100644 index 0000000..4f3a4de --- /dev/null +++ b/.github/docs/workflows/android/cloud-release-firebase.md @@ -0,0 +1,56 @@ + + +# Android Release (Firebase) + +**Source:** [`workflows/android-cloud-release-firebaseAppDistribution.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-release-firebaseAppDistribution.yml) +**Runner:** `ubuntu-latest` + +*Android Release to Firebase App Distribution* + +## Usage + +```yaml +jobs: + android-release-firebase: + uses: futuredapp/.github/.github/workflows/android-cloud-release-firebaseAppDistribution.yml@main + with: + TEST_GRADLE_TASKS: '...' + PACKAGE_GRADLE_TASK: '...' + UPLOAD_GRADLE_TASK: '...' + APP_DISTRIBUTION_GROUPS: '...' + secrets: + APP_DISTRIBUTION_SERVICE_ACCOUNT: ${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `TEST_GRADLE_TASKS` | `string` | Yes | — | A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest` | +| `PACKAGE_GRADLE_TASK` | `string` | Yes | — | A Gradle task for packaging universal APK, eg. 'packageEnterpriseUniversalApk' | +| `UPLOAD_GRADLE_TASK` | `string` | Yes | — | A Gradle task for uploading APK, for example `appDistributionUploadEnterprise` | +| `APP_DISTRIBUTION_GROUPS` | `string` | Yes | — | Comma-separated list of app distribution group IDs | +| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | +| `VERSION_NAME` | `string` | No | — | Version name. Example: '1.X.X-snapshot' | +| `BUILD_NUMBER_OFFSET` | `number` | No | `0` | Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers. | +| `RELEASE_NOTES` | `string` | No | `${{ github.event.head_commit.message }}` | Release notes for this build | +| `KMP_FLAVOR` | `string` | No | `test` | KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects | +| `SECRET_PROPERTIES_FILE` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | +| `JAVA_VERSION` | `string` | No | `17` | Java version to use | +| `JAVA_DISTRIBUTION` | `string` | No | `zulu` | Java distribution to use | +| `GRADLE_OPTS` | `string` | No | — | Gradle options | +| `TIMEOUT_MINUTES` | `number` | No | `30` | Job timeout in minutes | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `APP_DISTRIBUTION_SERVICE_ACCOUNT` | Yes | JSON key of service account with permissions to upload build to Firebase App Distribution | +| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | +| `SECRET_PROPERTIES` | No | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | + +## Internal Actions Used + +- [`Setup Environment`](../../actions/android/setup-environment.md) +- [`Build Firebase`](../../actions/android/build-firebase.md) + diff --git a/.github/docs/workflows/android/cloud-release-googleplay.md b/.github/docs/workflows/android/cloud-release-googleplay.md new file mode 100644 index 0000000..d4f7055 --- /dev/null +++ b/.github/docs/workflows/android/cloud-release-googleplay.md @@ -0,0 +1,60 @@ + + +# Android Release (Google Play) + +**Source:** [`workflows/android-cloud-release-googlePlay.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-release-googlePlay.yml) +**Runner:** `ubuntu-latest` + +*Android Release to Google Play* + +## Usage + +```yaml +jobs: + android-release-google-play: + uses: futuredapp/.github/.github/workflows/android-cloud-release-googlePlay.yml@main + with: + VERSION_NAME: '...' + BUNDLE_GRADLE_TASK: '...' + GOOGLE_PLAY_APPLICATION_ID: '...' + GOOGLE_PLAY_WHATSNEW_DIRECTORY: '...' + secrets: + SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} + SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} + SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} + GOOGLE_PLAY_PUBLISH_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_PUBLISH_SERVICE_ACCOUNT }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `VERSION_NAME` | `string` | Yes | — | Build version name | +| `BUNDLE_GRADLE_TASK` | `string` | Yes | — | A Gradle task for assembling app bundle, for example `bundleRelease` | +| `GOOGLE_PLAY_APPLICATION_ID` | `string` | Yes | — | Google Play applicationId | +| `GOOGLE_PLAY_WHATSNEW_DIRECTORY` | `string` | Yes | — | Path to directory with changelog files according to documentation in https://github.com/r0adkll/upload-google-play | +| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | +| `BUILD_NUMBER_OFFSET` | `number` | No | `0` | Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers. | +| `KMP_FLAVOR` | `string` | No | `prod` | KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects | +| `SECRET_PROPERTIES_FILE` | `string` | No | `secrets.properties` | A path to file that fill be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | +| `CHANGES_NOT_SENT_FOR_REVIEW` | `boolean` | No | `False` | A changesNotSentForReview Google Play flag. Enable when last google review failed, disable when last review was successful. | +| `TIMEOUT_MINUTES` | `number` | No | `30` | Job timeout in minutes | +| `JAVA_VERSION` | `string` | No | `17` | Java version to use | +| `JAVA_DISTRIBUTION` | `string` | No | `zulu` | Java distribution to use | +| `GRADLE_OPTS` | `string` | No | — | Gradle options | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `SIGNING_KEYSTORE_PASSWORD` | Yes | Password to provided keystore | +| `SIGNING_KEY_ALIAS` | Yes | Alias of the signing key in the provided keystore | +| `SIGNING_KEY_PASSWORD` | Yes | Password to the key in the provided keystore | +| `GOOGLE_PLAY_PUBLISH_SERVICE_ACCOUNT` | Yes | JSON key of service account with permissions to upload build to Google Play | +| `SECRET_PROPERTIES` | No | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | + +## Internal Actions Used + +- [`Setup Environment`](../../actions/android/setup-environment.md) +- [`Build Google Play`](../../actions/android/build-googleplay.md) + diff --git a/.github/docs/workflows/android/index.md b/.github/docs/workflows/android/index.md new file mode 100644 index 0000000..f7034a8 --- /dev/null +++ b/.github/docs/workflows/android/index.md @@ -0,0 +1,14 @@ + + +# Android Workflows + +Reusable GitHub Actions workflows for Android projects. + +| Name | Description | +|------|-------------| +| [Android PR Check](cloud-check.md) | Android Pull Request Check | +| [Android Nightly Build](cloud-nightly-build.md) | Android nightly build | +| [Android Release (Firebase)](cloud-release-firebase.md) | Android Release to Firebase App Distribution | +| [Android Release (Google Play)](cloud-release-googleplay.md) | Android Release to Google Play | +| [Android Generate Baseline Profiles](cloud-generate-baseline-profiles.md) | Generate baseline profiles | + diff --git a/.github/docs/workflows/index.md b/.github/docs/workflows/index.md new file mode 100644 index 0000000..8ebc4cb --- /dev/null +++ b/.github/docs/workflows/index.md @@ -0,0 +1,14 @@ + + +# Workflows + +All reusable GitHub Actions workflows organized by platform. + +| Name | Description | +|------|-------------| +| [iOS Workflows](ios/index.md) | 5 workflow(s) | +| [iOS + KMP Workflows](ios-kmp/index.md) | 3 workflow(s) | +| [Android Workflows](android/index.md) | 5 workflow(s) | +| [KMP Workflows](kmp/index.md) | 2 workflow(s) | +| [Universal Workflows](universal/index.md) | 3 workflow(s) | + diff --git a/.github/docs/workflows/ios-kmp/index.md b/.github/docs/workflows/ios-kmp/index.md new file mode 100644 index 0000000..62564b6 --- /dev/null +++ b/.github/docs/workflows/ios-kmp/index.md @@ -0,0 +1,12 @@ + + +# iOS + KMP Workflows + +Reusable GitHub Actions workflows for iOS + KMP projects. + +| Name | Description | +|------|-------------| +| [iOS KMP Test](selfhosted-test.md) | iOS KMP Self-hosted Test | +| [iOS KMP Build](selfhosted-build.md) | iOS KMP Self-hosted Build | +| [iOS KMP Release](selfhosted-release.md) | iOS KMP Self-hosted Release | + diff --git a/.github/docs/workflows/ios-kmp/selfhosted-build.md b/.github/docs/workflows/ios-kmp/selfhosted-build.md new file mode 100644 index 0000000..588456e --- /dev/null +++ b/.github/docs/workflows/ios-kmp/selfhosted-build.md @@ -0,0 +1,60 @@ + + +# iOS KMP Build + +**Source:** [`workflows/ios-kmp-selfhosted-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-kmp-selfhosted-build.yml) +**Runner:** `Self-hosted` + +*iOS KMP Self-hosted Build* + +## Usage + +```yaml +jobs: + ios-kmp-build: + uses: futuredapp/.github/.github/workflows/ios-kmp-selfhosted-build.yml@main + secrets: + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} + APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} + APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | +| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | +| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | +| `kmp_swift_package_integration` | `boolean` | No | `False` | Whether KMP is integrated in Xcode project as a Swift Package | +| `kmp_swift_package_path` | `string` | No | `iosApp/shared/KMP` | If `swift_package_integration` is 'true', then specifies a location of local Swift Package with Makefile. Example: 'iosApp/shared/KMP` | +| `kmp_swift_package_flavor` | `string` | No | `prod` | Build flavor of KMP Package | +| `java_version` | `string` | No | `17` | Java version to use | +| `java_distribution` | `string` | No | `zulu` | Java distribution to use | +| `xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | +| `required_keys` | `string` | No | — | Comma-separated list of required keys. | +| `changelog` | `string` | No | `${{ github.event.pull_request.title }}` | Will be used as TestFlight changelog | +| `custom_build_path` | `string` | No | — | Path to directory containing Fastfile. If not specified, uses iosApp. Example: iosApp/appA | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. + | +| `APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. + | +| `APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. + | +| `APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. + | +| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | +| `SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). + | + +## Internal Actions Used + +- [`Setup Environment`](../../actions/android/setup-environment.md) +- [`KMP Build`](../../actions/ios/kmp-build.md) + diff --git a/.github/docs/workflows/ios-kmp/selfhosted-release.md b/.github/docs/workflows/ios-kmp/selfhosted-release.md new file mode 100644 index 0000000..37077d9 --- /dev/null +++ b/.github/docs/workflows/ios-kmp/selfhosted-release.md @@ -0,0 +1,57 @@ + + +# iOS KMP Release + +**Source:** [`workflows/ios-kmp-selfhosted-release.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-kmp-selfhosted-release.yml) +**Runner:** `Self-hosted` + +*iOS KMP Self-hosted Release* + +## Usage + +```yaml +jobs: + ios-kmp-release: + uses: futuredapp/.github/.github/workflows/ios-kmp-selfhosted-release.yml@main + secrets: + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} + APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} + APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | +| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | +| `kmp_swift_package_integration` | `boolean` | No | `False` | Whether KMP is integrated in Xcode project as a Swift Package | +| `kmp_swift_package_path` | `string` | No | `iosApp/shared/KMP` | If `swift_package_integration` is 'true', then specifies a location of local Swift Package with Makefile. Example: 'iosApp/shared/KMP` | +| `kmp_swift_package_flavor` | `string` | No | `prod` | Build flavor of KMP Package | +| `java_version` | `string` | No | `17` | Java version to use | +| `java_distribution` | `string` | No | `zulu` | Java distribution to use | +| `xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | +| `required_keys` | `string` | No | — | Comma-separated list of required keys. | +| `custom_build_path` | `string` | No | — | Path to directory containing Fastfile. If not specified, uses iosApp. Example: iosApp/appA | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. + | +| `APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. + | +| `APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. + | +| `APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. + | +| `SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). + | + +## Internal Actions Used + +- [`Export Secrets`](../../actions/ios/export-secrets.md) +- [`Fastlane Release`](../../actions/ios/fastlane-release.md) + diff --git a/.github/docs/workflows/ios-kmp/selfhosted-test.md b/.github/docs/workflows/ios-kmp/selfhosted-test.md new file mode 100644 index 0000000..f2e0cbc --- /dev/null +++ b/.github/docs/workflows/ios-kmp/selfhosted-test.md @@ -0,0 +1,42 @@ + + +# iOS KMP Test + +**Source:** [`workflows/ios-kmp-selfhosted-test.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-kmp-selfhosted-test.yml) +**Runner:** `Self-hosted` + +*iOS KMP Self-hosted Test* + +## Usage + +```yaml +jobs: + ios-kmp-test: + uses: futuredapp/.github/.github/workflows/ios-kmp-selfhosted-test.yml@main +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | +| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | +| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | +| `kmp_swift_package_integration` | `boolean` | No | `False` | Whether KMP is integrated in Xcode project as a Swift Package | +| `kmp_swift_package_path` | `string` | No | `iosApp/shared/KMP` | If `swift_package_integration` is 'true', then specifies a location of local Swift Package with Makefile. Example: 'iosApp/shared/KMP` | +| `kmp_swift_package_flavor` | `string` | No | `dev` | Build flavor of KMP Package | +| `java_version` | `string` | No | `17` | Java version to use | +| `java_distribution` | `string` | No | `zulu` | Java distribution to use | +| `custom_build_path` | `string` | No | — | Path to directory containing Fastfile. If not specified, uses iosApp. Example: iosApp/appA | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | +| `GITHUB_TOKEN_DANGER` | No | GitHub token for Danger. Must have permissions to read and write issues and pull requests. | + +## Internal Actions Used + +- [`Fastlane Test`](../../actions/ios/fastlane-test.md) + diff --git a/.github/docs/workflows/ios/index.md b/.github/docs/workflows/ios/index.md new file mode 100644 index 0000000..21a2dd9 --- /dev/null +++ b/.github/docs/workflows/ios/index.md @@ -0,0 +1,14 @@ + + +# iOS Workflows + +Reusable GitHub Actions workflows for iOS projects. + +| Name | Description | +|------|-------------| +| [iOS Test](selfhosted-test.md) | iOS Self-hosted Test | +| [iOS Nightly Build](selfhosted-nightly-build.md) | iOS Self-hosted Nightly Build | +| [iOS On-Demand Build](selfhosted-on-demand-build.md) | iOS Self-hosted On-Demand Build | +| [iOS Release](selfhosted-release.md) | iOS Self-hosted Release | +| [iOS Build (Deprecated)](selfhosted-build.md) | Deprecated Build (use ios-selfhosted-nightly-build) | + diff --git a/.github/docs/workflows/ios/selfhosted-build.md b/.github/docs/workflows/ios/selfhosted-build.md new file mode 100644 index 0000000..628a17d --- /dev/null +++ b/.github/docs/workflows/ios/selfhosted-build.md @@ -0,0 +1,52 @@ + + +# iOS Build (Deprecated) + +!!! warning "Deprecated" + Use `ios-selfhosted-nightly-build` instead. + +**Source:** [`workflows/ios-selfhosted-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-build.yml) +**Runner:** `Self-hosted` + +*Deprecated Build (use ios-selfhosted-nightly-build)* + +## Usage + +```yaml +jobs: + ios-build-deprecated: + uses: futuredapp/.github/.github/workflows/ios-selfhosted-build.yml@main + secrets: + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} + APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} + APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | +| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | +| `runner_label` | `string` | No | `self-hosted` | The custom label for the self-hosted runner to use for the build job. | +| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | +| `xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | +| `secret_properties` | `string` | No | — | Secrets in the format KEY = VALUE (one per line). | +| `required_keys` | `string` | No | — | Comma-separated list of required keys. | +| `changelog_fallback_lookback` | `string` | No | `24 hours` | The amount of time to look back for merge commits when no previous build commit is found. Default is 24 hours. | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. | +| `APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. | +| `APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. | +| `APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. | +| `SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). | + +## Internal Actions Used + +- [`iOS Nightly Build`](selfhosted-nightly-build.md) + diff --git a/.github/docs/workflows/ios/selfhosted-nightly-build.md b/.github/docs/workflows/ios/selfhosted-nightly-build.md new file mode 100644 index 0000000..7a403d8 --- /dev/null +++ b/.github/docs/workflows/ios/selfhosted-nightly-build.md @@ -0,0 +1,54 @@ + + +# iOS Nightly Build + +**Source:** [`workflows/ios-selfhosted-nightly-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-nightly-build.yml) +**Runner:** `Self-hosted` + +*iOS Self-hosted Nightly Build* + +## Usage + +```yaml +jobs: + ios-nightly-build: + uses: futuredapp/.github/.github/workflows/ios-selfhosted-nightly-build.yml@main + secrets: + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} + APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} + APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `runner_label` | `string` | No | `self-hosted` | The custom label for the self-hosted runner to use for the build job. | +| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | +| `checkout_depth` | `number` | No | `100` | The depth of the git history to fetch for changelog generation. | +| `changelog_fallback_lookback` | `string` | No | `24 hours` | The amount of time to look back for merge commits when no previous build commit is found. Default is 24 hours. | +| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | +| `xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | +| `required_keys` | `string` | No | — | Comma-separated list of required keys. | +| `custom_values` | `string` | No | `24 hours` | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | +| `jira_transition` | `string` | No | `Testing` | The name of the JIRA transition to apply to tickets found in merged branches. | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. | +| `APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. | +| `APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. | +| `APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. | +| `SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). | +| `JIRA_CONTEXT` | No | JIRA context for transitioning tickets. | + +## Internal Actions Used + +- [`Detect Changes & Changelog`](../../actions/utility/detect-changes-changelog.md) +- [`Export Secrets`](../../actions/ios/export-secrets.md) +- [`Fastlane Beta`](../../actions/ios/fastlane-beta.md) +- [`JIRA Transition Tickets`](../../actions/utility/jira-transition-tickets.md) + diff --git a/.github/docs/workflows/ios/selfhosted-on-demand-build.md b/.github/docs/workflows/ios/selfhosted-on-demand-build.md new file mode 100644 index 0000000..0e0ee35 --- /dev/null +++ b/.github/docs/workflows/ios/selfhosted-on-demand-build.md @@ -0,0 +1,52 @@ + + +# iOS On-Demand Build + +**Source:** [`workflows/ios-selfhosted-on-demand-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-on-demand-build.yml) +**Runner:** `Self-hosted` + +*iOS Self-hosted On-Demand Build* + +## Usage + +```yaml +jobs: + ios-on-demand-build: + uses: futuredapp/.github/.github/workflows/ios-selfhosted-on-demand-build.yml@main + secrets: + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} + APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} + APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `runner_label` | `string` | No | `self-hosted` | The custom label for the self-hosted runner to use for the build job. | +| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | +| `changelog` | `string` | No | — | Will be used as TestFlight changelog | +| `checkout_depth` | `number` | No | `100` | The depth of the git history to fetch for changelog generation. | +| `changelog_fallback_lookback` | `string` | No | `24 hours` | The amount of time to look back for merge commits when no previous build commit is found. Default is 24 hours. | +| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | +| `xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | +| `required_keys` | `string` | No | — | Comma-separated list of required keys. | +| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. | +| `APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. | +| `APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. | +| `APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. | +| `SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). | + +## Internal Actions Used + +- [`Detect Changes & Changelog`](../../actions/utility/detect-changes-changelog.md) +- [`Export Secrets`](../../actions/ios/export-secrets.md) +- [`Fastlane Beta`](../../actions/ios/fastlane-beta.md) + diff --git a/.github/docs/workflows/ios/selfhosted-release.md b/.github/docs/workflows/ios/selfhosted-release.md new file mode 100644 index 0000000..50016ee --- /dev/null +++ b/.github/docs/workflows/ios/selfhosted-release.md @@ -0,0 +1,48 @@ + + +# iOS Release + +**Source:** [`workflows/ios-selfhosted-release.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-release.yml) +**Runner:** `Self-hosted` + +*iOS Self-hosted Release* + +## Usage + +```yaml +jobs: + ios-release: + uses: futuredapp/.github/.github/workflows/ios-selfhosted-release.yml@main + secrets: + MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} + APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} + APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} + APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `runner_label` | `string` | No | `self-hosted` | The custom label for the self-hosted runner to use for the build job. | +| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | +| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | +| `xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | +| `required_keys` | `string` | No | — | Comma-separated list of required keys. | +| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. | +| `APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. | +| `APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. | +| `APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. | +| `SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). | + +## Internal Actions Used + +- [`Export Secrets`](../../actions/ios/export-secrets.md) +- [`Fastlane Release`](../../actions/ios/fastlane-release.md) + diff --git a/.github/docs/workflows/ios/selfhosted-test.md b/.github/docs/workflows/ios/selfhosted-test.md new file mode 100644 index 0000000..d5cb9bf --- /dev/null +++ b/.github/docs/workflows/ios/selfhosted-test.md @@ -0,0 +1,36 @@ + + +# iOS Test + +**Source:** [`workflows/ios-selfhosted-test.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-test.yml) +**Runner:** `Self-hosted` + +*iOS Self-hosted Test* + +## Usage + +```yaml +jobs: + ios-test: + uses: futuredapp/.github/.github/workflows/ios-selfhosted-test.yml@main +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | +| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | +| `runner_label` | `string` | No | `self-hosted` | The custom label for the self-hosted runner to use for the build job. | +| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `GITHUB_TOKEN_DANGER` | No | GitHub token for Danger. Must have permissions to read and write issues and pull requests. | + +## Internal Actions Used + +- [`Fastlane Test`](../../actions/ios/fastlane-test.md) + diff --git a/.github/docs/workflows/kmp/cloud-detect-changes.md b/.github/docs/workflows/kmp/cloud-detect-changes.md new file mode 100644 index 0000000..255c7b4 --- /dev/null +++ b/.github/docs/workflows/kmp/cloud-detect-changes.md @@ -0,0 +1,34 @@ + + +# KMP Detect Changes + +**Source:** [`workflows/kmp-cloud-detect-changes.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/kmp-cloud-detect-changes.yml) +**Runner:** `ubuntu-latest` + +*Detect Changes* + +## Usage + +```yaml +jobs: + kmp-detect-changes: + uses: futuredapp/.github/.github/workflows/kmp-cloud-detect-changes.yml@main +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | + +## Outputs + +| Name | Description | +|------|-------------| +| `iosFiles` | Whether files affecting iOS build changed (all files except those in androidApp/) | +| `androidFiles` | Whether files affecting Android build changed (all files except those in iosApp/) | + +## Internal Actions Used + +- [`KMP Detect Changes`](../../actions/utility/kmp-detect-changes.md) + diff --git a/.github/docs/workflows/kmp/combined-nightly-build.md b/.github/docs/workflows/kmp/combined-nightly-build.md new file mode 100644 index 0000000..746ce0b --- /dev/null +++ b/.github/docs/workflows/kmp/combined-nightly-build.md @@ -0,0 +1,79 @@ + + +# KMP Combined Nightly Build + +**Source:** [`workflows/kmp-combined-nightly-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/kmp-combined-nightly-build.yml) +**Runner:** `Self-hosted + ubuntu-latest` + +*KMP nightly build* + +## Usage + +```yaml +jobs: + kmp-combined-nightly-build: + uses: futuredapp/.github/.github/workflows/kmp-combined-nightly-build.yml@main + with: + ANDROID_TEST_GRADLE_TASK: '...' + ANDROID_PACKAGE_GRADLE_TASK: '...' + ANDROID_UPLOAD_GRADLE_TASK: '...' + KMP_FLAVOR: '...' + FIREBASE_APP_DISTRIBUTION_GROUPS: '...' + secrets: + FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT }} + IOS_MATCH_PASSWORD: ${{ secrets.IOS_MATCH_PASSWORD }} + IOS_APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.IOS_APP_STORE_CONNECT_API_KEY_KEY }} + IOS_APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.IOS_APP_STORE_CONNECT_API_KEY_KEY_ID }} + IOS_APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.IOS_APP_STORE_CONNECT_API_KEY_ISSUER_ID }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `ANDROID_TEST_GRADLE_TASK` | `string` | Yes | — | A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest` | +| `ANDROID_PACKAGE_GRADLE_TASK` | `string` | Yes | — | A Gradle task for packaging universal APK, eg. 'packageEnterpriseUniversalApk' | +| `ANDROID_UPLOAD_GRADLE_TASK` | `string` | Yes | — | A Gradle task for uploading APK, for example `appDistributionUploadEnterprise` | +| `KMP_FLAVOR` | `string` | Yes | — | KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects | +| `FIREBASE_APP_DISTRIBUTION_GROUPS` | `string` | Yes | — | Comma-separated list of app distribution group IDs | +| `TIMEOUT_MINUTES` | `number` | No | `30` | Job timeout in minutes | +| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | +| `GRADLE_OPTS` | `string` | No | — | Gradle options | +| `JAVA_VERSION` | `string` | No | `17` | Java version to use | +| `JAVA_DISTRIBUTION` | `string` | No | `zulu` | Java distribution to use | +| `ANDROID_VERSION_NAME` | `string` | No | — | Version name. Example: '1.X.X-snapshot' | +| `ANDROID_BUILD_NUMBER_OFFSET` | `number` | No | `0` | Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers. | +| `KMP_SWIFT_PACKAGE_INTEGRATION` | `boolean` | No | `False` | Whether KMP is integrated in Xcode project as a Swift Package | +| `KMP_SWIFT_PACKAGE_PATH` | `string` | No | — | If `KMP_SWIFT_PACKAGE_INTEGRATION` is 'true', then specifies a location of local Swift Package with Makefile. Example: 'iosApp/shared/KMP` | +| `ANDROID_SECRET_PROPERTIES_FILE` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'android_secret_properties' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | +| `IOS_SECRET_XCCONFIG_PATH` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | +| `IOS_SECRET_REQUIRED_KEYS` | `string` | No | — | Comma-separated list of required secret keys. | +| `IOS_CUSTOM_BUILD_PATH` | `string` | No | — | Path to a folder where the iOS code is located and where bundle exec fastlane is executed. This should be relative to iosApp folder | +| `IOS_CUSTOM_VALUES` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | +| `CHANGELOG_DEBUG` | `boolean` | No | `False` | Enable debug mode for changelog generation. Default is false. | +| `CHANGELOG_CHECKOUT_DEPTH` | `number` | No | `100` | The depth of the git history to fetch for changelog generation. Default is 100. | +| `CHANGELOG_FALLBACK_LOOKBACK` | `string` | No | `24 hours` | The amount of time to look back for merge commits when no previous build commit is found. Default is 24 hours. | +| `JIRA_TRANSITION` | `string` | No | `Testing` | The name of the JIRA transition to apply to tickets found in merged branches. | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT` | Yes | JSON key of service account with permissions to upload build to Firebase App Distribution | +| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | +| `ANDROID_SECRET_PROPERTIES` | No | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'ANDROID_SECRET_PROPERTIES_FILE' input. | +| `IOS_SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). | +| `IOS_MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. | +| `IOS_APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. | +| `IOS_APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. | +| `IOS_APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. | +| `JIRA_CONTEXT` | No | JIRA context for transitioning tickets. | + +## Internal Actions Used + +- [`Detect Changes & Changelog`](../../actions/utility/detect-changes-changelog.md) +- [`Setup Environment`](../../actions/android/setup-environment.md) +- [`KMP Build`](../../actions/ios/kmp-build.md) +- [`Build Firebase`](../../actions/android/build-firebase.md) +- [`JIRA Transition Tickets`](../../actions/utility/jira-transition-tickets.md) + diff --git a/.github/docs/workflows/kmp/index.md b/.github/docs/workflows/kmp/index.md new file mode 100644 index 0000000..1d0aea2 --- /dev/null +++ b/.github/docs/workflows/kmp/index.md @@ -0,0 +1,11 @@ + + +# KMP Workflows + +Reusable GitHub Actions workflows for KMP projects. + +| Name | Description | +|------|-------------| +| [KMP Detect Changes](cloud-detect-changes.md) | Detect Changes | +| [KMP Combined Nightly Build](combined-nightly-build.md) | KMP nightly build | + diff --git a/.github/docs/workflows/universal/cloud-backup.md b/.github/docs/workflows/universal/cloud-backup.md new file mode 100644 index 0000000..a333986 --- /dev/null +++ b/.github/docs/workflows/universal/cloud-backup.md @@ -0,0 +1,37 @@ + + +# Cloud Backup + +**Source:** [`workflows/universal-cloud-backup.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/universal-cloud-backup.yml) +**Runner:** `ubuntu-latest` + +*Backup* + +## Usage + +```yaml +jobs: + cloud-backup: + uses: futuredapp/.github/.github/workflows/universal-cloud-backup.yml@main + with: + remote: '...' + secrets: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `host` | `string` | No | `github.com` | Host name. | +| `remote` | `string` | Yes | — | Remote SSH repository address. | +| `use_git_lfs` | `boolean` | No | — | Whether to download Git-LFS files. | +| `push_tags` | `boolean` | No | `False` | Whether to also push tags to backup origin. | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `SSH_PRIVATE_KEY` | Yes | Key for accessing repo with Apple certificates and provisioning profiles and repo with imported Fastlane lanes. + | + diff --git a/.github/docs/workflows/universal/index.md b/.github/docs/workflows/universal/index.md new file mode 100644 index 0000000..e73d7b1 --- /dev/null +++ b/.github/docs/workflows/universal/index.md @@ -0,0 +1,12 @@ + + +# Universal Workflows + +Reusable GitHub Actions workflows for Universal projects. + +| Name | Description | +|------|-------------| +| [Cloud Backup](cloud-backup.md) | Backup | +| [Self-hosted Backup](selfhosted-backup.md) | Backup | +| [Workflows Lint](workflows-lint.md) | Check Pull Request | + diff --git a/.github/docs/workflows/universal/selfhosted-backup.md b/.github/docs/workflows/universal/selfhosted-backup.md new file mode 100644 index 0000000..c29ac19 --- /dev/null +++ b/.github/docs/workflows/universal/selfhosted-backup.md @@ -0,0 +1,37 @@ + + +# Self-hosted Backup + +**Source:** [`workflows/universal-selfhosted-backup.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/universal-selfhosted-backup.yml) +**Runner:** `Self-hosted` + +*Backup* + +## Usage + +```yaml +jobs: + self-hosted-backup: + uses: futuredapp/.github/.github/workflows/universal-selfhosted-backup.yml@main + with: + remote: '...' + secrets: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} +``` + +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +| `host` | `string` | No | `github.com` | Host name. | +| `remote` | `string` | Yes | — | Remote SSH repository address. | +| `use_git_lfs` | `boolean` | No | — | Whether to download Git-LFS files. | +| `push_tags` | `boolean` | No | `False` | Whether to also push tags to backup origin. | + +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +| `SSH_PRIVATE_KEY` | Yes | Key for accessing repo with Apple certificates and provisioning profiles and repo with imported Fastlane lanes. + | + diff --git a/.github/docs/workflows/universal/workflows-lint.md b/.github/docs/workflows/universal/workflows-lint.md new file mode 100644 index 0000000..c19a2f6 --- /dev/null +++ b/.github/docs/workflows/universal/workflows-lint.md @@ -0,0 +1,12 @@ + + +# Workflows Lint + +!!! info "Internal Workflow" + This is not a reusable workflow — it runs directly on `pull_request` events in this repository. + +**Source:** [`workflows/workflows-lint.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/workflows-lint.yml) +**Runner:** `ubuntu-latest` + +*Check Pull Request* + diff --git a/.github/mkdocs.yml b/.github/mkdocs.yml new file mode 100644 index 0000000..1d3370a --- /dev/null +++ b/.github/mkdocs.yml @@ -0,0 +1,138 @@ +#$schema: https://squidfunk.github.io/mkdocs-material/schema.json + +site_name: Futured CI/CD Workflows +site_url: https://futuredapp.github.io/.github +theme: + name: material + language: en + + custom_dir: docs/overrides + + favicon: assets/images/favicon.png + icon: + logo: futured/logo + + font: + code: JetBrains Mono + + palette: + # Palette toggle for automatic mode + - media: "(prefers-color-scheme)" + toggle: + icon: material/brightness-auto + name: Switch to auto mode + + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + toggle: + icon: material/brightness-4 + name: Switch to light mode + + features: + - navigation.instant + - navigation.tabs + - navigation.sections + - navigation.indexes + - navigation.expand + - content.code.copy + +plugins: + - search + - glightbox: + background: light-dark(white, black) + shadow: false + +nav: + - Home: index.md + - Workflows: + - Overview: workflows/index.md + - iOS: + - workflows/ios/index.md + - Test: workflows/ios/selfhosted-test.md + - Nightly Build: workflows/ios/selfhosted-nightly-build.md + - On-demand Build: workflows/ios/selfhosted-on-demand-build.md + - Release: workflows/ios/selfhosted-release.md + - Build (Deprecated): workflows/ios/selfhosted-build.md + - iOS + KMP: + - workflows/ios-kmp/index.md + - Test: workflows/ios-kmp/selfhosted-test.md + - Build: workflows/ios-kmp/selfhosted-build.md + - Release: workflows/ios-kmp/selfhosted-release.md + - Android: + - workflows/android/index.md + - PR Check: workflows/android/cloud-check.md + - Nightly Build: workflows/android/cloud-nightly-build.md + - Release (Firebase): workflows/android/cloud-release-firebase.md + - Release (Google Play): workflows/android/cloud-release-googleplay.md + - Generate Baseline Profiles: workflows/android/cloud-generate-baseline-profiles.md + - KMP: + - workflows/kmp/index.md + - Detect Changes: workflows/kmp/cloud-detect-changes.md + - Combined Nightly Build: workflows/kmp/combined-nightly-build.md + - Universal: + - workflows/universal/index.md + - Workflows Lint: workflows/universal/workflows-lint.md + - Cloud Backup: workflows/universal/cloud-backup.md + - Self-hosted Backup: workflows/universal/selfhosted-backup.md + - Actions: + - Overview: actions/index.md + - Android: + - actions/android/index.md + - Setup Environment: actions/android/setup-environment.md + - Check: actions/android/check.md + - Build Firebase: actions/android/build-firebase.md + - Build Google Play: actions/android/build-googleplay.md + - Generate Baseline Profiles: actions/android/generate-baseline-profiles.md + - iOS: + - actions/ios/index.md + - Export Secrets: actions/ios/export-secrets.md + - Fastlane Test: actions/ios/fastlane-test.md + - Fastlane Beta: actions/ios/fastlane-beta.md + - Fastlane Release: actions/ios/fastlane-release.md + - KMP Build: actions/ios/kmp-build.md + - Utility: + - actions/utility/index.md + - KMP Detect Changes: actions/utility/kmp-detect-changes.md + - Detect Changes & Changelog: actions/utility/detect-changes-changelog.md + - JIRA Transition Tickets: actions/utility/jira-transition-tickets.md + +copyright: Made with ❤️‍🔥 at Futured + +extra: + social: + - icon: material/web + name: Web + link: https://www.futured.app + - icon: fontawesome/brands/github + name: GitHub + link: https://www.github.com/futuredapp + - icon: fontawesome/brands/linkedin + name: LinkedIn + link: https://www.linkedin.com/company/futuredapps + - icon: fontawesome/brands/instagram + name: Instagram + link: https://www.instagram.com/futuredapps + +extra_css: + - stylesheets/extra.css + +markdown_extensions: + - admonition + - attr_list + - md_in_html + - pymdownx.details + - pymdownx.superfences + - pymdownx.tabbed: + alternate_style: true + - pymdownx.blocks.caption + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg diff --git a/.github/requirements-docs.txt b/.github/requirements-docs.txt new file mode 100644 index 0000000..a13d819 --- /dev/null +++ b/.github/requirements-docs.txt @@ -0,0 +1,5 @@ +mkdocs>=1.6,<2 +mkdocs-material>=9.5 +mkdocs-glightbox>=0.4 +Jinja2>=3.1 +PyYAML>=6.0 diff --git a/.github/scripts/config.py b/.github/scripts/config.py new file mode 100644 index 0000000..f824af9 --- /dev/null +++ b/.github/scripts/config.py @@ -0,0 +1,229 @@ +"""Registry mapping every workflow and action to its documentation metadata.""" + +from __future__ import annotations + +# Categories: ios, ios-kmp, android, kmp, universal, utility + +WORKFLOWS: dict[str, dict] = { + "ios-selfhosted-test": { + "source": "workflows/ios-selfhosted-test.yml", + "category": "ios", + "title": "iOS Test", + "output": "docs/workflows/ios/selfhosted-test.md", + "runner": "Self-hosted", + }, + "ios-selfhosted-nightly-build": { + "source": "workflows/ios-selfhosted-nightly-build.yml", + "category": "ios", + "title": "iOS Nightly Build", + "output": "docs/workflows/ios/selfhosted-nightly-build.md", + "runner": "Self-hosted", + }, + "ios-selfhosted-on-demand-build": { + "source": "workflows/ios-selfhosted-on-demand-build.yml", + "category": "ios", + "title": "iOS On-Demand Build", + "output": "docs/workflows/ios/selfhosted-on-demand-build.md", + "runner": "Self-hosted", + }, + "ios-selfhosted-release": { + "source": "workflows/ios-selfhosted-release.yml", + "category": "ios", + "title": "iOS Release", + "output": "docs/workflows/ios/selfhosted-release.md", + "runner": "Self-hosted", + }, + "ios-selfhosted-build": { + "source": "workflows/ios-selfhosted-build.yml", + "category": "ios", + "title": "iOS Build (Deprecated)", + "output": "docs/workflows/ios/selfhosted-build.md", + "runner": "Self-hosted", + "deprecated": True, + "deprecated_message": "Use `ios-selfhosted-nightly-build` instead.", + }, + "ios-kmp-selfhosted-test": { + "source": "workflows/ios-kmp-selfhosted-test.yml", + "category": "ios-kmp", + "title": "iOS KMP Test", + "output": "docs/workflows/ios-kmp/selfhosted-test.md", + "runner": "Self-hosted", + }, + "ios-kmp-selfhosted-build": { + "source": "workflows/ios-kmp-selfhosted-build.yml", + "category": "ios-kmp", + "title": "iOS KMP Build", + "output": "docs/workflows/ios-kmp/selfhosted-build.md", + "runner": "Self-hosted", + }, + "ios-kmp-selfhosted-release": { + "source": "workflows/ios-kmp-selfhosted-release.yml", + "category": "ios-kmp", + "title": "iOS KMP Release", + "output": "docs/workflows/ios-kmp/selfhosted-release.md", + "runner": "Self-hosted", + }, + "android-cloud-check": { + "source": "workflows/android-cloud-check.yml", + "category": "android", + "title": "Android PR Check", + "output": "docs/workflows/android/cloud-check.md", + "runner": "ubuntu-latest", + }, + "android-cloud-nightly-build": { + "source": "workflows/android-cloud-nightly-build.yml", + "category": "android", + "title": "Android Nightly Build", + "output": "docs/workflows/android/cloud-nightly-build.md", + "runner": "ubuntu-latest", + }, + "android-cloud-release-firebase": { + "source": "workflows/android-cloud-release-firebaseAppDistribution.yml", + "category": "android", + "title": "Android Release (Firebase)", + "output": "docs/workflows/android/cloud-release-firebase.md", + "runner": "ubuntu-latest", + }, + "android-cloud-release-googleplay": { + "source": "workflows/android-cloud-release-googlePlay.yml", + "category": "android", + "title": "Android Release (Google Play)", + "output": "docs/workflows/android/cloud-release-googleplay.md", + "runner": "ubuntu-latest", + }, + "android-cloud-generate-baseline-profiles": { + "source": "workflows/android-cloud-generate-baseline-profiles.yml", + "category": "android", + "title": "Android Generate Baseline Profiles", + "output": "docs/workflows/android/cloud-generate-baseline-profiles.md", + "runner": "ubuntu-latest", + }, + "kmp-cloud-detect-changes": { + "source": "workflows/kmp-cloud-detect-changes.yml", + "category": "kmp", + "title": "KMP Detect Changes", + "output": "docs/workflows/kmp/cloud-detect-changes.md", + "runner": "ubuntu-latest", + }, + "kmp-combined-nightly-build": { + "source": "workflows/kmp-combined-nightly-build.yml", + "category": "kmp", + "title": "KMP Combined Nightly Build", + "output": "docs/workflows/kmp/combined-nightly-build.md", + "runner": "Self-hosted + ubuntu-latest", + }, + "universal-cloud-backup": { + "source": "workflows/universal-cloud-backup.yml", + "category": "universal", + "title": "Cloud Backup", + "output": "docs/workflows/universal/cloud-backup.md", + "runner": "ubuntu-latest", + }, + "universal-selfhosted-backup": { + "source": "workflows/universal-selfhosted-backup.yml", + "category": "universal", + "title": "Self-hosted Backup", + "output": "docs/workflows/universal/selfhosted-backup.md", + "runner": "Self-hosted", + }, + "workflows-lint": { + "source": "workflows/workflows-lint.yml", + "category": "universal", + "title": "Workflows Lint", + "output": "docs/workflows/universal/workflows-lint.md", + "runner": "ubuntu-latest", + "not_reusable": True, + }, +} + +ACTIONS: dict[str, dict] = { + "android-setup-environment": { + "source": "actions/android-setup-environment/action.yml", + "category": "android", + "title": "Setup Environment", + "output": "docs/actions/android/setup-environment.md", + }, + "android-check": { + "source": "actions/android-check/action.yml", + "category": "android", + "title": "Android Check", + "output": "docs/actions/android/check.md", + }, + "android-build-firebase": { + "source": "actions/android-build-firebase/action.yml", + "category": "android", + "title": "Build Firebase", + "output": "docs/actions/android/build-firebase.md", + }, + "android-build-googleplay": { + "source": "actions/android-build-googlePlay/action.yml", + "category": "android", + "title": "Build Google Play", + "output": "docs/actions/android/build-googleplay.md", + }, + "android-generate-baseline-profiles": { + "source": "actions/android-generate-baseline-profiles/action.yml", + "category": "android", + "title": "Generate Baseline Profiles", + "output": "docs/actions/android/generate-baseline-profiles.md", + }, + "ios-export-secrets": { + "source": "actions/ios-export-secrets/action.yml", + "category": "ios", + "title": "Export Secrets", + "output": "docs/actions/ios/export-secrets.md", + }, + "ios-fastlane-test": { + "source": "actions/ios-fastlane-test/action.yml", + "category": "ios", + "title": "Fastlane Test", + "output": "docs/actions/ios/fastlane-test.md", + }, + "ios-fastlane-beta": { + "source": "actions/ios-fastlane-beta/action.yml", + "category": "ios", + "title": "Fastlane Beta", + "output": "docs/actions/ios/fastlane-beta.md", + }, + "ios-fastlane-release": { + "source": "actions/ios-fastlane-release/action.yml", + "category": "ios", + "title": "Fastlane Release", + "output": "docs/actions/ios/fastlane-release.md", + }, + "ios-kmp-build": { + "source": "actions/ios-kmp-build/action.yml", + "category": "ios", + "title": "KMP Build", + "output": "docs/actions/ios/kmp-build.md", + }, + "kmp-detect-changes": { + "source": "actions/kmp-detect-changes/action.yml", + "category": "utility", + "title": "KMP Detect Changes", + "output": "docs/actions/utility/kmp-detect-changes.md", + }, + "universal-detect-changes-and-generate-changelog": { + "source": "actions/universal-detect-changes-and-generate-changelog/action.yml", + "category": "utility", + "title": "Detect Changes & Changelog", + "output": "docs/actions/utility/detect-changes-changelog.md", + "readme": "actions/universal-detect-changes-and-generate-changelog/README.md", + }, + "jira-transition-tickets": { + "source": "actions/jira-transition-tickets/action.yml", + "category": "utility", + "title": "JIRA Transition Tickets", + "output": "docs/actions/utility/jira-transition-tickets.md", + "readme": "actions/jira-transition-tickets/README.md", + }, +} + +CATEGORY_LABELS: dict[str, str] = { + "ios": "iOS", + "ios-kmp": "iOS + KMP", + "android": "Android", + "kmp": "KMP", + "universal": "Universal", + "utility": "Utility", +} diff --git a/.github/scripts/enrichers/__init__.py b/.github/scripts/enrichers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.github/scripts/enrichers/ai_enricher.py b/.github/scripts/enrichers/ai_enricher.py new file mode 100644 index 0000000..d338899 --- /dev/null +++ b/.github/scripts/enrichers/ai_enricher.py @@ -0,0 +1,76 @@ +"""AI-powered documentation enricher (stub). + +This enricher is a documented no-op that serves as the integration point +for future AI-generated documentation. When activated, it will use an LLM +to generate additional descriptions, usage tips, and examples for workflows +and actions that lack hand-written READMEs. + +Activation: + Pass ``--enrich`` flag to ``generate-docs.py`` AND set the + ``AI_DOCS_API_KEY`` environment variable. Optionally provide + ``--ai-config path/to/config.json`` with the following structure:: + + { + "api_key_env": "AI_DOCS_API_KEY", + "model": "claude-sonnet-4-20250514", + "prompt_template": "scripts/templates/ai_prompt.txt", + "max_tokens": 1024 + } + +Implementation notes for future activation: + 1. Read the source YAML file content for the spec. + 2. Construct a prompt from the template with the YAML content and + existing enrichment results. + 3. Call the AI API to generate additional documentation. + 4. Parse the response into an EnrichmentResult. +""" + +from __future__ import annotations + +import json +import os +from pathlib import Path + +from .base import BaseEnricher, EnrichmentResult + + +class AIEnricher(BaseEnricher): + """AI-powered enricher — currently a documented no-op. + + Activates only when ``--enrich`` flag is passed AND the API key + environment variable is set. Without both conditions met, this + enricher silently produces empty results. + """ + + def __init__( + self, + enabled: bool = False, + config_path: str | Path | None = None, + ) -> None: + self._enabled = enabled + self._config: dict = {} + + if config_path and Path(config_path).exists(): + with open(config_path) as f: + self._config = json.load(f) + + # Check for API key + api_key_env = self._config.get("api_key_env", "AI_DOCS_API_KEY") + self._api_key = os.environ.get(api_key_env, "") + + def name(self) -> str: + return "ai" + + def can_enrich(self, spec: object, config: dict) -> bool: + return self._enabled and bool(self._api_key) + + def enrich( + self, + spec: object, + config: dict, + prior_results: list[EnrichmentResult], + ) -> EnrichmentResult: + # Stub — no-op until AI integration is implemented. + # When implementing, use self._config for model/prompt settings + # and self._api_key for authentication. + return EnrichmentResult() diff --git a/.github/scripts/enrichers/base.py b/.github/scripts/enrichers/base.py new file mode 100644 index 0000000..10a9057 --- /dev/null +++ b/.github/scripts/enrichers/base.py @@ -0,0 +1,54 @@ +"""Abstract base class for documentation enrichers.""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass, field + + +@dataclass +class EnrichmentResult: + """Result of an enrichment pass on a workflow or action spec.""" + + additional_description: str | None = None + usage_tips: str | None = None + examples: list[str] = field(default_factory=list) + + +class BaseEnricher(ABC): + """Interface for documentation enrichers. + + Enrichers augment auto-generated documentation with additional content + beyond what the YAML metadata provides. They run in sequence; each + enricher receives the results of all prior enrichers so it can avoid + duplicating content. + """ + + @abstractmethod + def name(self) -> str: + """Human-readable name for this enricher.""" + ... + + @abstractmethod + def can_enrich(self, spec: object, config: dict) -> bool: + """Return True if this enricher has content to add for the given spec.""" + ... + + @abstractmethod + def enrich( + self, + spec: object, + config: dict, + prior_results: list[EnrichmentResult], + ) -> EnrichmentResult: + """Produce enrichment content for the given spec. + + Args: + spec: A WorkflowSpec or ActionSpec instance. + config: The registry entry dict for this item. + prior_results: Results from enrichers that ran before this one. + + Returns: + An EnrichmentResult with any additional content to inject. + """ + ... diff --git a/.github/scripts/enrichers/readme_enricher.py b/.github/scripts/enrichers/readme_enricher.py new file mode 100644 index 0000000..6f93edd --- /dev/null +++ b/.github/scripts/enrichers/readme_enricher.py @@ -0,0 +1,108 @@ +"""Enricher that extracts content from existing README files.""" + +from __future__ import annotations + +import re +from pathlib import Path + +from .base import BaseEnricher, EnrichmentResult + + +class ReadmeEnricher(BaseEnricher): + """Extracts documentation sections from existing README.md files. + + For actions that already have hand-written READMEs (e.g. + jira-transition-tickets, universal-detect-changes-and-generate-changelog), + this enricher pulls in sections that go beyond what the YAML metadata + provides — How It Works, Usage Examples, Testing, architecture details, etc. + """ + + def __init__(self, base_dir: str | Path) -> None: + self._base_dir = Path(base_dir) + + def name(self) -> str: + return "readme" + + def can_enrich(self, spec: object, config: dict) -> bool: + return "readme" in config + + def enrich( + self, + spec: object, + config: dict, + prior_results: list[EnrichmentResult], + ) -> EnrichmentResult: + readme_path = self._base_dir / config["readme"] + if not readme_path.exists(): + return EnrichmentResult() + + content = readme_path.read_text(encoding="utf-8") + + # Extract sections beyond Inputs/Outputs (those are already in the + # generated tables). Keep sections like How It Works, Usage Examples, + # Testing, Features, Scripts, etc. + skip_headings = { + "inputs", + "outputs", + "overview", # Usually duplicates description + } + + sections: list[str] = [] + current_section: list[str] = [] + current_heading = "" + in_skip = False + in_code_fence = False + + for line in content.splitlines(): + # Track fenced code blocks to avoid matching headings inside them + if line.startswith("```"): + in_code_fence = not in_code_fence + if not in_skip: + current_section.append(line) + continue + + if in_code_fence: + if not in_skip: + current_section.append(line) + continue + + heading_match = re.match(r"^(#{1,3})\s+(.+)", line) + if heading_match: + # Save previous section if not skipped + if current_section and not in_skip: + sections.append("\n".join(current_section)) + + current_heading = heading_match.group(2).strip() + heading_key = re.sub(r"[`*]", "", current_heading).lower() + + # Skip the title line (first H1) and known metadata sections + level = len(heading_match.group(1)) + if level == 1: + in_skip = True + current_section = [] + continue + + in_skip = heading_key in skip_headings + current_section = [line] if not in_skip else [] + else: + if not in_skip: + current_section.append(line) + + # Don't forget the last section + if current_section and not in_skip: + sections.append("\n".join(current_section)) + + additional = "\n\n".join(s.strip() for s in sections if s.strip()) + + # Extract usage examples separately + examples: list[str] = [] + example_blocks = re.findall( + r"```yaml\n(.*?)```", content, re.DOTALL + ) + for block in example_blocks: + examples.append(block.strip()) + + return EnrichmentResult( + additional_description=additional if additional else None, + examples=examples if examples else [], + ) diff --git a/.github/scripts/generate-docs.py b/.github/scripts/generate-docs.py new file mode 100644 index 0000000..9ea276f --- /dev/null +++ b/.github/scripts/generate-docs.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +"""Generate documentation markdown files from workflow and action YAML specs. + +Usage: + python scripts/generate-docs.py [--enrich] [--ai-config PATH] + +Pipeline: + 1. Load config registry + 2. Parse all workflow YAMLs + action YAMLs + 3. Run enricher pipeline (README enricher always; AI enricher if --enrich) + 4. Render markdown via Jinja2 templates + 5. Write generated .md files to docs/ + 6. Generate category index pages +""" + +from __future__ import annotations + +import argparse +import os +import sys +from collections import defaultdict +from pathlib import Path + +# Ensure the scripts package is importable +SCRIPT_DIR = Path(__file__).resolve().parent +ROOT_DIR = SCRIPT_DIR.parent +sys.path.insert(0, str(ROOT_DIR)) + +from scripts.config import ACTIONS, CATEGORY_LABELS, WORKFLOWS +from scripts.enrichers.ai_enricher import AIEnricher +from scripts.enrichers.base import BaseEnricher, EnrichmentResult +from scripts.enrichers.readme_enricher import ReadmeEnricher +from scripts.parsers.action_parser import parse_action +from scripts.parsers.workflow_parser import parse_workflow +from scripts.renderers.markdown_renderer import ( + render_action, + render_index, + render_workflow, +) + + +def _run_enrichers( + enrichers: list[BaseEnricher], + spec: object, + config: dict, +) -> list[EnrichmentResult]: + """Run all enrichers in sequence, passing prior results forward.""" + results: list[EnrichmentResult] = [] + for enricher in enrichers: + if enricher.can_enrich(spec, config): + result = enricher.enrich(spec, config, results) + results.append(result) + return results + + +def _build_workflow_index_items( + category: str, + configs: list[tuple[str, dict]], + specs: dict, +) -> list[dict]: + """Build index items for workflows in a category.""" + items = [] + for key, cfg in configs: + spec = specs.get(key) + # Relative link from index to the workflow page + filename = Path(cfg["output"]).name + items.append( + { + "title": cfg["title"], + "link": filename, + "description": spec.name if spec else cfg["title"], + } + ) + return items + + +def _build_action_index_items( + category: str, + configs: list[tuple[str, dict]], + specs: dict, +) -> list[dict]: + """Build index items for actions in a category.""" + items = [] + for key, cfg in configs: + spec = specs.get(key) + filename = Path(cfg["output"]).name + items.append( + { + "title": cfg["title"], + "link": filename, + "description": spec.description if spec else cfg["title"], + } + ) + return items + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate documentation site") + parser.add_argument( + "--enrich", + action="store_true", + help="Enable AI enricher (requires AI_DOCS_API_KEY env var)", + ) + parser.add_argument( + "--ai-config", + type=str, + default=None, + help="Path to AI enricher configuration JSON", + ) + parser.add_argument( + "--ref", + type=str, + default="main", + help="Git ref for usage snippets (e.g. 'main' or '2.1.0')", + ) + args = parser.parse_args() + + templates_dir = SCRIPT_DIR / "templates" + + # Initialize enrichers + enrichers: list[BaseEnricher] = [ + ReadmeEnricher(ROOT_DIR), + AIEnricher(enabled=args.enrich, config_path=args.ai_config), + ] + + # ------------------------------------------------------------------- + # Parse all workflow YAML files + # ------------------------------------------------------------------- + print("Parsing workflows...") + workflow_specs = {} + for key, cfg in WORKFLOWS.items(): + source = ROOT_DIR / cfg["source"] + if not source.exists(): + print(f" WARNING: {source} not found, skipping {key}") + continue + spec = parse_workflow(source) + workflow_specs[key] = spec + print(f" Parsed: {key} ({spec.name})") + + # ------------------------------------------------------------------- + # Parse all action YAML files + # ------------------------------------------------------------------- + print("\nParsing actions...") + action_specs = {} + for key, cfg in ACTIONS.items(): + source = ROOT_DIR / cfg["source"] + if not source.exists(): + print(f" WARNING: {source} not found, skipping {key}") + continue + spec = parse_action(source) + action_specs[key] = spec + print(f" Parsed: {key} ({spec.name})") + + # ------------------------------------------------------------------- + # Render workflow pages + # ------------------------------------------------------------------- + print("\nRendering workflow pages...") + for key, cfg in WORKFLOWS.items(): + spec = workflow_specs.get(key) + if not spec: + continue + enrichments = _run_enrichers(enrichers, spec, cfg) + path = render_workflow(spec, cfg, enrichments, templates_dir, ROOT_DIR, ref=args.ref) + print(f" Written: {path.relative_to(ROOT_DIR)}") + + # ------------------------------------------------------------------- + # Render action pages + # ------------------------------------------------------------------- + print("\nRendering action pages...") + for key, cfg in ACTIONS.items(): + spec = action_specs.get(key) + if not spec: + continue + enrichments = _run_enrichers(enrichers, spec, cfg) + path = render_action(spec, cfg, enrichments, templates_dir, ROOT_DIR, ref=args.ref) + print(f" Written: {path.relative_to(ROOT_DIR)}") + + # ------------------------------------------------------------------- + # Generate category index pages + # ------------------------------------------------------------------- + print("\nGenerating index pages...") + + # Group workflows by category + wf_by_category: dict[str, list[tuple[str, dict]]] = defaultdict(list) + for key, cfg in WORKFLOWS.items(): + wf_by_category[cfg["category"]].append((key, cfg)) + + # Group actions by category + act_by_category: dict[str, list[tuple[str, dict]]] = defaultdict(list) + for key, cfg in ACTIONS.items(): + act_by_category[cfg["category"]].append((key, cfg)) + + # Workflow category index pages + for category, entries in wf_by_category.items(): + label = CATEGORY_LABELS.get(category, category.title()) + items = _build_workflow_index_items(category, entries, workflow_specs) + index_path = ROOT_DIR / "docs" / "workflows" / category / "index.md" + render_index( + title=f"{label} Workflows", + description=f"Reusable GitHub Actions workflows for {label} projects.", + items=items, + templates_dir=templates_dir, + output_path=index_path, + ) + print(f" Written: {index_path.relative_to(ROOT_DIR)}") + + # Action category index pages + for category, entries in act_by_category.items(): + label = CATEGORY_LABELS.get(category, category.title()) + items = _build_action_index_items(category, entries, action_specs) + index_path = ROOT_DIR / "docs" / "actions" / category / "index.md" + render_index( + title=f"{label} Actions", + description=f"Composite GitHub Actions for {label} projects.", + items=items, + templates_dir=templates_dir, + output_path=index_path, + ) + print(f" Written: {index_path.relative_to(ROOT_DIR)}") + + # Top-level workflow index + all_wf_items = [] + for category in ["ios", "ios-kmp", "android", "kmp", "universal"]: + label = CATEGORY_LABELS.get(category, category.title()) + all_wf_items.append( + { + "title": f"{label} Workflows", + "link": f"{category}/index.md", + "description": f"{len(wf_by_category.get(category, []))} workflow(s)", + } + ) + render_index( + title="Workflows", + description="All reusable GitHub Actions workflows organized by platform.", + items=all_wf_items, + templates_dir=templates_dir, + output_path=ROOT_DIR / "docs" / "workflows" / "index.md", + ) + print(f" Written: docs/workflows/index.md") + + # Top-level action index + all_act_items = [] + for category in ["android", "ios", "utility"]: + label = CATEGORY_LABELS.get(category, category.title()) + all_act_items.append( + { + "title": f"{label} Actions", + "link": f"{category}/index.md", + "description": f"{len(act_by_category.get(category, []))} action(s)", + } + ) + render_index( + title="Actions", + description="All composite GitHub Actions organized by platform.", + items=all_act_items, + templates_dir=templates_dir, + output_path=ROOT_DIR / "docs" / "actions" / "index.md", + ) + print(f" Written: docs/actions/index.md") + + # ------------------------------------------------------------------- + # Summary + # ------------------------------------------------------------------- + total_wf = len(workflow_specs) + total_act = len(action_specs) + print(f"\nDone! Generated {total_wf} workflow pages + {total_act} action pages.") + + +if __name__ == "__main__": + main() diff --git a/.github/scripts/parsers/__init__.py b/.github/scripts/parsers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.github/scripts/parsers/action_parser.py b/.github/scripts/parsers/action_parser.py new file mode 100644 index 0000000..a547fa3 --- /dev/null +++ b/.github/scripts/parsers/action_parser.py @@ -0,0 +1,60 @@ +"""Parser for composite GitHub Actions (action.yml) files.""" + +from __future__ import annotations + +from pathlib import Path + +import yaml + +from .types import ActionSpec, InputSpec, OutputSpec + + +def parse_action(path: str | Path) -> ActionSpec: + """Parse a composite action YAML file into an ActionSpec.""" + path = Path(path) + with open(path) as f: + data = yaml.safe_load(f) + + name = data.get("name", path.parent.name) + description = data.get("description", "") + + inputs = _parse_inputs(data.get("inputs") or {}) + outputs = _parse_outputs(data.get("outputs") or {}) + + return ActionSpec( + name=name, + description=description, + source_path=str(path), + inputs=inputs, + outputs=outputs, + ) + + +def _parse_inputs(raw: dict) -> list[InputSpec]: + inputs = [] + for name, spec in raw.items(): + spec = spec or {} + default = spec.get("default") + inputs.append( + InputSpec( + name=name, + description=spec.get("description", ""), + type=spec.get("type", "string"), + required=spec.get("required", False), + default=str(default) if default is not None else None, + ) + ) + return inputs + + +def _parse_outputs(raw: dict) -> list[OutputSpec]: + outputs = [] + for name, spec in raw.items(): + spec = spec or {} + outputs.append( + OutputSpec( + name=name, + description=spec.get("description", ""), + ) + ) + return outputs diff --git a/.github/scripts/parsers/types.py b/.github/scripts/parsers/types.py new file mode 100644 index 0000000..c167615 --- /dev/null +++ b/.github/scripts/parsers/types.py @@ -0,0 +1,46 @@ +"""Shared dataclasses for parsed workflow and action specifications.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass +class InputSpec: + name: str + description: str + type: str = "string" + required: bool = False + default: str | None = None + + +@dataclass +class SecretSpec: + name: str + description: str + required: bool = False + + +@dataclass +class OutputSpec: + name: str + description: str + + +@dataclass +class WorkflowSpec: + name: str + source_path: str + inputs: list[InputSpec] = field(default_factory=list) + secrets: list[SecretSpec] = field(default_factory=list) + outputs: list[OutputSpec] = field(default_factory=list) + jobs: dict[str, dict] = field(default_factory=dict) + + +@dataclass +class ActionSpec: + name: str + description: str + source_path: str + inputs: list[InputSpec] = field(default_factory=list) + outputs: list[OutputSpec] = field(default_factory=list) diff --git a/.github/scripts/parsers/workflow_parser.py b/.github/scripts/parsers/workflow_parser.py new file mode 100644 index 0000000..5b0d27f --- /dev/null +++ b/.github/scripts/parsers/workflow_parser.py @@ -0,0 +1,106 @@ +"""Parser for reusable GitHub Actions workflow files (on.workflow_call).""" + +from __future__ import annotations + +from pathlib import Path + +import yaml + +from .types import InputSpec, OutputSpec, SecretSpec, WorkflowSpec + + +def parse_workflow(path: str | Path) -> WorkflowSpec: + """Parse a reusable workflow YAML file into a WorkflowSpec.""" + path = Path(path) + with open(path) as f: + data = yaml.safe_load(f) + + name = data.get("name", path.stem) + + # Extract workflow_call trigger definition + on_block = data.get("on") or data.get(True) or {} + workflow_call = {} + if isinstance(on_block, dict): + workflow_call = on_block.get("workflow_call", {}) or {} + + inputs = _parse_inputs(workflow_call.get("inputs") or {}) + secrets = _parse_secrets(workflow_call.get("secrets") or {}) + outputs = _parse_outputs(workflow_call.get("outputs") or {}) + jobs = _parse_jobs(data.get("jobs") or {}) + + return WorkflowSpec( + name=name, + source_path=str(path), + inputs=inputs, + secrets=secrets, + outputs=outputs, + jobs=jobs, + ) + + +def _parse_inputs(raw: dict) -> list[InputSpec]: + inputs = [] + for name, spec in raw.items(): + spec = spec or {} + default = spec.get("default") + inputs.append( + InputSpec( + name=name, + description=spec.get("description", ""), + type=spec.get("type", "string"), + required=spec.get("required", False), + default=str(default) if default is not None else None, + ) + ) + return inputs + + +def _parse_secrets(raw: dict) -> list[SecretSpec]: + secrets = [] + for name, spec in raw.items(): + spec = spec or {} + secrets.append( + SecretSpec( + name=name, + description=spec.get("description", ""), + required=spec.get("required", False), + ) + ) + return secrets + + +def _parse_outputs(raw: dict) -> list[OutputSpec]: + outputs = [] + for name, spec in raw.items(): + spec = spec or {} + outputs.append( + OutputSpec( + name=name, + description=spec.get("description", ""), + ) + ) + return outputs + + +def _parse_jobs(raw: dict) -> dict[str, dict]: + jobs = {} + for job_name, job_spec in raw.items(): + job_spec = job_spec or {} + job_info: dict = { + "runs-on": job_spec.get("runs-on", ""), + } + + # Collect all 'uses' references from steps and job-level reuse + uses_refs: list[str] = [] + if "uses" in job_spec: + uses_refs.append(job_spec["uses"]) + + for step in job_spec.get("steps") or []: + if "uses" in step: + uses_refs.append(step["uses"]) + + if uses_refs: + job_info["uses"] = uses_refs + + jobs[job_name] = job_info + return jobs diff --git a/.github/scripts/renderers/__init__.py b/.github/scripts/renderers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/.github/scripts/renderers/markdown_renderer.py b/.github/scripts/renderers/markdown_renderer.py new file mode 100644 index 0000000..26d584e --- /dev/null +++ b/.github/scripts/renderers/markdown_renderer.py @@ -0,0 +1,220 @@ +"""Renders parsed specs + enrichment results into markdown files via Jinja2.""" + +from __future__ import annotations + +import os +import re +from pathlib import Path + +from jinja2 import Environment, FileSystemLoader + +from scripts.config import ACTIONS, CATEGORY_LABELS, WORKFLOWS +from scripts.enrichers.base import EnrichmentResult +from scripts.parsers.types import ActionSpec, InputSpec, WorkflowSpec + + +def _build_env(templates_dir: str | Path) -> Environment: + return Environment( + loader=FileSystemLoader(str(templates_dir)), + keep_trailing_newline=True, + trim_blocks=True, + lstrip_blocks=True, + ) + + +def _usage_placeholder(inp: InputSpec) -> str: + """Generate a placeholder value for the usage snippet.""" + if inp.default is not None: + return inp.default + type_map = { + "boolean": "true", + "number": "0", + } + return type_map.get(inp.type, "'...'") + + +def _resolve_action_link(uses_ref: str, from_output_path: str) -> dict | None: + """Resolve a uses: reference to a cross-link if it's an internal action.""" + # Match futuredapp/.github/.github/actions/@... + match = re.match( + r"futuredapp/\.github/\.github/actions/([^@]+)@", uses_ref + ) + if not match: + # Also match internal workflow references + wf_match = re.match( + r"futuredapp/\.github/\.github/workflows/([^@]+)@", uses_ref + ) + if not wf_match: + return None + wf_file = wf_match.group(1) + # Find matching workflow config + for _key, cfg in WORKFLOWS.items(): + if cfg["source"].endswith(wf_file): + rel = os.path.relpath(cfg["output"], os.path.dirname(from_output_path)) + return {"name": cfg["title"], "link": rel} + return None + + action_name = match.group(1) + for _key, cfg in ACTIONS.items(): + if action_name in cfg["source"]: + rel = os.path.relpath(cfg["output"], os.path.dirname(from_output_path)) + return {"name": cfg["title"], "link": rel} + return None + + +def render_workflow( + spec: WorkflowSpec, + config: dict, + enrichments: list[EnrichmentResult], + templates_dir: str | Path, + output_base: str | Path, + ref: str = "main", +) -> Path: + """Render a workflow spec to a markdown file.""" + env = _build_env(templates_dir) + template = env.get_template("workflow.md.j2") + + # Prepare inputs with usage placeholders + inputs = [] + required_inputs = [] + for inp in spec.inputs: + inp_dict = { + "name": inp.name, + "type": inp.type, + "required": inp.required, + "default": inp.default, + "description": inp.description, + "usage_placeholder": _usage_placeholder(inp), + } + inputs.append(inp_dict) + if inp.required: + required_inputs.append(inp_dict) + + required_secrets = [s for s in spec.secrets if s.required] + + # Resolve internal action cross-links + internal_actions: list[dict] = [] + seen_actions: set[str] = set() + for _job_name, job_info in spec.jobs.items(): + for uses_ref in job_info.get("uses", []): + link = _resolve_action_link(uses_ref, config["output"]) + if link and link["name"] not in seen_actions: + internal_actions.append(link) + seen_actions.add(link["name"]) + + # Merge enrichment results + enrichment_parts: list[str] = [] + for er in enrichments: + if er.additional_description: + enrichment_parts.append(er.additional_description) + if er.usage_tips: + enrichment_parts.append(er.usage_tips) + enrichment_text = "\n\n".join(enrichment_parts) if enrichment_parts else "" + + # Generate a sensible job name for the usage snippet + usage_job_name = config.get("title", "build").lower().replace(" ", "-") + usage_job_name = re.sub(r"[^a-z0-9-]", "", usage_job_name) + + rendered = template.render( + title=config["title"], + source_path=config["source"], + runner=config.get("runner", ""), + deprecated=config.get("deprecated", False), + deprecated_message=config.get("deprecated_message", ""), + not_reusable=config.get("not_reusable", False), + spec=spec, + inputs=inputs, + required_inputs=required_inputs, + secrets=spec.secrets, + required_secrets=required_secrets, + outputs=spec.outputs, + internal_actions=internal_actions, + enrichment=enrichment_text, + usage_job_name=usage_job_name, + ref=ref, + ) + + output_path = Path(output_base) / config["output"] + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(rendered, encoding="utf-8") + return output_path + + +def render_action( + spec: ActionSpec, + config: dict, + enrichments: list[EnrichmentResult], + templates_dir: str | Path, + output_base: str | Path, + ref: str = "main", +) -> Path: + """Render an action spec to a markdown file.""" + env = _build_env(templates_dir) + template = env.get_template("action.md.j2") + + inputs = [] + required_inputs = [] + for inp in spec.inputs: + inp_dict = { + "name": inp.name, + "type": inp.type, + "required": inp.required, + "default": inp.default, + "description": inp.description, + "usage_placeholder": _usage_placeholder(inp), + } + inputs.append(inp_dict) + if inp.required: + required_inputs.append(inp_dict) + + # Merge enrichment results + enrichment_parts: list[str] = [] + for er in enrichments: + if er.additional_description: + enrichment_parts.append(er.additional_description) + if er.usage_tips: + enrichment_parts.append(er.usage_tips) + enrichment_text = "\n\n".join(enrichment_parts) if enrichment_parts else "" + + # Action path (e.g. actions/android-setup-environment) + action_path = str(Path(config["source"]).parent) + + rendered = template.render( + title=config["title"], + source_path=config["source"], + spec=spec, + inputs=inputs, + required_inputs=required_inputs, + outputs=spec.outputs, + enrichment=enrichment_text, + action_path=action_path, + ref=ref, + ) + + output_path = Path(output_base) / config["output"] + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(rendered, encoding="utf-8") + return output_path + + +def render_index( + title: str, + description: str, + items: list[dict], + templates_dir: str | Path, + output_path: str | Path, +) -> Path: + """Render a category index page.""" + env = _build_env(templates_dir) + template = env.get_template("index.md.j2") + + rendered = template.render( + title=title, + description=description, + items=items, + ) + + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(rendered, encoding="utf-8") + return output_path diff --git a/.github/scripts/templates/action.md.j2 b/.github/scripts/templates/action.md.j2 new file mode 100644 index 0000000..fd88c99 --- /dev/null +++ b/.github/scripts/templates/action.md.j2 @@ -0,0 +1,48 @@ + + +# {{ title }} + +**Source:** [`{{ source_path }}`](https://github.com/futuredapp/.github/blob/main/.github/{{ source_path }}) + +{{ spec.description }} + +{% if inputs %} +## Usage + +```yaml +- uses: futuredapp/.github/.github/{{ action_path }}@{{ ref }} +{% if required_inputs %} + with: +{% for inp in required_inputs %} + {{ inp.name }}: {{ inp.usage_placeholder }} +{% endfor %} +{% endif %} +``` + +{% endif %} +{% if inputs %} +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +{% for inp in inputs %} +| `{{ inp.name }}` | `{{ inp.type }}` | {{ "Yes" if inp.required else "No" }} | {{ "`" + inp.default + "`" if inp.default is not none and inp.default != "" else "—" }} | {{ inp.description }} | +{% endfor %} + +{% endif %} +{% if outputs %} +## Outputs + +| Name | Description | +|------|-------------| +{% for out in outputs %} +| `{{ out.name }}` | {{ out.description }} | +{% endfor %} + +{% endif %} +{% if enrichment %} +## Additional Details + +{{ enrichment }} + +{% endif %} diff --git a/.github/scripts/templates/index.md.j2 b/.github/scripts/templates/index.md.j2 new file mode 100644 index 0000000..67e231a --- /dev/null +++ b/.github/scripts/templates/index.md.j2 @@ -0,0 +1,14 @@ + + +# {{ title }} + +{{ description }} + +{% if items %} +| Name | Description | +|------|-------------| +{% for item in items %} +| [{{ item.title }}]({{ item.link }}) | {{ item.description }} | +{% endfor %} + +{% endif %} diff --git a/.github/scripts/templates/workflow.md.j2 b/.github/scripts/templates/workflow.md.j2 new file mode 100644 index 0000000..89d3210 --- /dev/null +++ b/.github/scripts/templates/workflow.md.j2 @@ -0,0 +1,89 @@ + + +# {{ title }} + +{% if deprecated %} +!!! warning "Deprecated" + {{ deprecated_message }} + +{% endif %} +{% if not_reusable %} +!!! info "Internal Workflow" + This is not a reusable workflow — it runs directly on `pull_request` events in this repository. + +{% endif %} +**Source:** [`{{ source_path }}`](https://github.com/futuredapp/.github/blob/main/.github/{{ source_path }}) +{% if runner %} +**Runner:** `{{ runner }}` +{% endif %} + +{% if spec.name != title %} +*{{ spec.name }}* +{% endif %} + +{% if inputs %} +## Usage + +```yaml +jobs: + {{ usage_job_name }}: + uses: futuredapp/.github/.github/{{ source_path }}@{{ ref }} +{% if required_inputs %} + with: +{% for inp in required_inputs %} + {{ inp.name }}: {{ inp.usage_placeholder }} +{% endfor %} +{% endif %} +{% if required_secrets %} + secrets: +{% for sec in required_secrets %} + {{ sec.name }}: {{"${{"}} secrets.{{ sec.name }} {{"}}"}} +{% endfor %} +{% endif %} +``` + +{% endif %} +{% if inputs %} +## Inputs + +| Name | Type | Required | Default | Description | +|------|------|----------|---------|-------------| +{% for inp in inputs %} +| `{{ inp.name }}` | `{{ inp.type }}` | {{ "Yes" if inp.required else "No" }} | {{ "`" + inp.default + "`" if inp.default is not none and inp.default != "" else "—" }} | {{ inp.description }} | +{% endfor %} + +{% endif %} +{% if secrets %} +## Secrets + +| Name | Required | Description | +|------|----------|-------------| +{% for sec in secrets %} +| `{{ sec.name }}` | {{ "Yes" if sec.required else "No" }} | {{ sec.description }} | +{% endfor %} + +{% endif %} +{% if outputs %} +## Outputs + +| Name | Description | +|------|-------------| +{% for out in outputs %} +| `{{ out.name }}` | {{ out.description }} | +{% endfor %} + +{% endif %} +{% if internal_actions %} +## Internal Actions Used + +{% for action in internal_actions %} +- [`{{ action.name }}`]({{ action.link }}) +{% endfor %} + +{% endif %} +{% if enrichment %} +## Additional Details + +{{ enrichment }} + +{% endif %} diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..da93b97 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,57 @@ +name: Deploy Documentation + +on: + push: + branches: [main] + tags: ['*'] + paths: + - '.github/workflows/*.yml' + - '.github/actions/*/action.yml' + - '.github/actions/*/README.md' + - 'docs/**' + - 'mkdocs.yml' + - 'scripts/**' + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: deploy-docs + cancel-in-progress: true + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + - uses: actions/setup-python@v5 + with: + python-version: '3.x' + - uses: actions/cache@v4 + with: + key: mkdocs-${{ hashFiles('requirements-docs.txt') }} + path: .cache + restore-keys: mkdocs- + - run: pip install -r requirements-docs.txt + - name: Determine version and ref + id: version + run: | + VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev") + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + if [[ "$GITHUB_REF" == refs/tags/* ]]; then + echo "ref=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" + else + echo "ref=main" >> "$GITHUB_OUTPUT" + fi + - name: Inject version into site title + run: | + sed -i "s/^site_name:.*/site_name: Futured CI\/CD Workflows ${{ steps.version.outputs.version }}/" mkdocs.yml + - run: python scripts/generate-docs.py --ref ${{ steps.version.outputs.ref }} + - run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - run: mkdocs gh-deploy --force diff --git a/.gitignore b/.gitignore index 4a6f485..90c9f29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .idea *.DS_Store +.github/.venv/ +.github/site/ From 58461d8f0b2dd3c9961313ce61d801555a06696a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20S=CC=8Cesta=CC=81k?= Date: Fri, 20 Feb 2026 11:54:44 +0000 Subject: [PATCH 02/10] fix(docs): Fix parser null guards, cross-link bug, hardcoded categories, and DRY violations - Guard against yaml.safe_load returning None on empty files in both parsers - Fix substring match bug in action cross-links (e.g. `build` matching `build-firebase`) - Derive top-level index categories from CATEGORY_LABELS instead of hardcoded lists - Extract duplicated _parse_inputs/_parse_outputs into shared types module Co-Authored-By: Claude Opus 4.6 --- .github/docs/actions/index.md | 2 +- .github/scripts/generate-docs.py | 10 +++-- .github/scripts/parsers/action_parser.py | 37 ++----------------- .github/scripts/parsers/types.py | 34 ++++++++++++++++- .github/scripts/parsers/workflow_parser.py | 37 ++----------------- .../scripts/renderers/markdown_renderer.py | 2 +- 6 files changed, 49 insertions(+), 73 deletions(-) diff --git a/.github/docs/actions/index.md b/.github/docs/actions/index.md index d0e7a72..1a9ba17 100644 --- a/.github/docs/actions/index.md +++ b/.github/docs/actions/index.md @@ -6,7 +6,7 @@ All composite GitHub Actions organized by platform. | Name | Description | |------|-------------| -| [Android Actions](android/index.md) | 5 action(s) | | [iOS Actions](ios/index.md) | 5 action(s) | +| [Android Actions](android/index.md) | 5 action(s) | | [Utility Actions](utility/index.md) | 3 action(s) | diff --git a/.github/scripts/generate-docs.py b/.github/scripts/generate-docs.py index 9ea276f..aec2e92 100644 --- a/.github/scripts/generate-docs.py +++ b/.github/scripts/generate-docs.py @@ -220,13 +220,14 @@ def main() -> None: # Top-level workflow index all_wf_items = [] - for category in ["ios", "ios-kmp", "android", "kmp", "universal"]: + wf_categories = [c for c in CATEGORY_LABELS if c in wf_by_category] + for category in wf_categories: label = CATEGORY_LABELS.get(category, category.title()) all_wf_items.append( { "title": f"{label} Workflows", "link": f"{category}/index.md", - "description": f"{len(wf_by_category.get(category, []))} workflow(s)", + "description": f"{len(wf_by_category[category])} workflow(s)", } ) render_index( @@ -240,13 +241,14 @@ def main() -> None: # Top-level action index all_act_items = [] - for category in ["android", "ios", "utility"]: + act_categories = [c for c in CATEGORY_LABELS if c in act_by_category] + for category in act_categories: label = CATEGORY_LABELS.get(category, category.title()) all_act_items.append( { "title": f"{label} Actions", "link": f"{category}/index.md", - "description": f"{len(act_by_category.get(category, []))} action(s)", + "description": f"{len(act_by_category[category])} action(s)", } ) render_index( diff --git a/.github/scripts/parsers/action_parser.py b/.github/scripts/parsers/action_parser.py index a547fa3..c500d10 100644 --- a/.github/scripts/parsers/action_parser.py +++ b/.github/scripts/parsers/action_parser.py @@ -6,7 +6,7 @@ import yaml -from .types import ActionSpec, InputSpec, OutputSpec +from .types import ActionSpec, parse_inputs, parse_outputs def parse_action(path: str | Path) -> ActionSpec: @@ -14,12 +14,13 @@ def parse_action(path: str | Path) -> ActionSpec: path = Path(path) with open(path) as f: data = yaml.safe_load(f) + data = data or {} name = data.get("name", path.parent.name) description = data.get("description", "") - inputs = _parse_inputs(data.get("inputs") or {}) - outputs = _parse_outputs(data.get("outputs") or {}) + inputs = parse_inputs(data.get("inputs") or {}) + outputs = parse_outputs(data.get("outputs") or {}) return ActionSpec( name=name, @@ -28,33 +29,3 @@ def parse_action(path: str | Path) -> ActionSpec: inputs=inputs, outputs=outputs, ) - - -def _parse_inputs(raw: dict) -> list[InputSpec]: - inputs = [] - for name, spec in raw.items(): - spec = spec or {} - default = spec.get("default") - inputs.append( - InputSpec( - name=name, - description=spec.get("description", ""), - type=spec.get("type", "string"), - required=spec.get("required", False), - default=str(default) if default is not None else None, - ) - ) - return inputs - - -def _parse_outputs(raw: dict) -> list[OutputSpec]: - outputs = [] - for name, spec in raw.items(): - spec = spec or {} - outputs.append( - OutputSpec( - name=name, - description=spec.get("description", ""), - ) - ) - return outputs diff --git a/.github/scripts/parsers/types.py b/.github/scripts/parsers/types.py index c167615..2ac77d1 100644 --- a/.github/scripts/parsers/types.py +++ b/.github/scripts/parsers/types.py @@ -1,4 +1,4 @@ -"""Shared dataclasses for parsed workflow and action specifications.""" +"""Shared dataclasses and helpers for parsed workflow and action specifications.""" from __future__ import annotations @@ -44,3 +44,35 @@ class ActionSpec: source_path: str inputs: list[InputSpec] = field(default_factory=list) outputs: list[OutputSpec] = field(default_factory=list) + + +def parse_inputs(raw: dict) -> list[InputSpec]: + """Parse a raw inputs dict into a list of InputSpec.""" + inputs = [] + for name, spec in raw.items(): + spec = spec or {} + default = spec.get("default") + inputs.append( + InputSpec( + name=name, + description=spec.get("description", ""), + type=spec.get("type", "string"), + required=spec.get("required", False), + default=str(default) if default is not None else None, + ) + ) + return inputs + + +def parse_outputs(raw: dict) -> list[OutputSpec]: + """Parse a raw outputs dict into a list of OutputSpec.""" + outputs = [] + for name, spec in raw.items(): + spec = spec or {} + outputs.append( + OutputSpec( + name=name, + description=spec.get("description", ""), + ) + ) + return outputs diff --git a/.github/scripts/parsers/workflow_parser.py b/.github/scripts/parsers/workflow_parser.py index 5b0d27f..bb06712 100644 --- a/.github/scripts/parsers/workflow_parser.py +++ b/.github/scripts/parsers/workflow_parser.py @@ -6,7 +6,7 @@ import yaml -from .types import InputSpec, OutputSpec, SecretSpec, WorkflowSpec +from .types import InputSpec, OutputSpec, SecretSpec, WorkflowSpec, parse_inputs, parse_outputs def parse_workflow(path: str | Path) -> WorkflowSpec: @@ -14,6 +14,7 @@ def parse_workflow(path: str | Path) -> WorkflowSpec: path = Path(path) with open(path) as f: data = yaml.safe_load(f) + data = data or {} name = data.get("name", path.stem) @@ -23,9 +24,9 @@ def parse_workflow(path: str | Path) -> WorkflowSpec: if isinstance(on_block, dict): workflow_call = on_block.get("workflow_call", {}) or {} - inputs = _parse_inputs(workflow_call.get("inputs") or {}) + inputs = parse_inputs(workflow_call.get("inputs") or {}) secrets = _parse_secrets(workflow_call.get("secrets") or {}) - outputs = _parse_outputs(workflow_call.get("outputs") or {}) + outputs = parse_outputs(workflow_call.get("outputs") or {}) jobs = _parse_jobs(data.get("jobs") or {}) return WorkflowSpec( @@ -38,23 +39,6 @@ def parse_workflow(path: str | Path) -> WorkflowSpec: ) -def _parse_inputs(raw: dict) -> list[InputSpec]: - inputs = [] - for name, spec in raw.items(): - spec = spec or {} - default = spec.get("default") - inputs.append( - InputSpec( - name=name, - description=spec.get("description", ""), - type=spec.get("type", "string"), - required=spec.get("required", False), - default=str(default) if default is not None else None, - ) - ) - return inputs - - def _parse_secrets(raw: dict) -> list[SecretSpec]: secrets = [] for name, spec in raw.items(): @@ -69,19 +53,6 @@ def _parse_secrets(raw: dict) -> list[SecretSpec]: return secrets -def _parse_outputs(raw: dict) -> list[OutputSpec]: - outputs = [] - for name, spec in raw.items(): - spec = spec or {} - outputs.append( - OutputSpec( - name=name, - description=spec.get("description", ""), - ) - ) - return outputs - - def _parse_jobs(raw: dict) -> dict[str, dict]: jobs = {} for job_name, job_spec in raw.items(): diff --git a/.github/scripts/renderers/markdown_renderer.py b/.github/scripts/renderers/markdown_renderer.py index 26d584e..93ff067 100644 --- a/.github/scripts/renderers/markdown_renderer.py +++ b/.github/scripts/renderers/markdown_renderer.py @@ -56,7 +56,7 @@ def _resolve_action_link(uses_ref: str, from_output_path: str) -> dict | None: action_name = match.group(1) for _key, cfg in ACTIONS.items(): - if action_name in cfg["source"]: + if cfg["source"] == f"actions/{action_name}/action.yml": rel = os.path.relpath(cfg["output"], os.path.dirname(from_output_path)) return {"name": cfg["title"], "link": rel} return None From 09c9b665191a2575311d06564e4a8c9139b954a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20S=CC=8Cesta=CC=81k?= Date: Fri, 20 Feb 2026 11:56:55 +0000 Subject: [PATCH 03/10] fix(docs): Add blank line between Source and Runner in workflow template Without a blank line separator, Markdown renders them as a single paragraph on one line. Co-Authored-By: Claude Opus 4.6 --- .github/docs/workflows/android/cloud-check.md | 1 + .../docs/workflows/android/cloud-generate-baseline-profiles.md | 1 + .github/docs/workflows/android/cloud-nightly-build.md | 1 + .github/docs/workflows/android/cloud-release-firebase.md | 1 + .github/docs/workflows/android/cloud-release-googleplay.md | 1 + .github/docs/workflows/ios-kmp/selfhosted-build.md | 1 + .github/docs/workflows/ios-kmp/selfhosted-release.md | 1 + .github/docs/workflows/ios-kmp/selfhosted-test.md | 1 + .github/docs/workflows/ios/selfhosted-build.md | 1 + .github/docs/workflows/ios/selfhosted-nightly-build.md | 1 + .github/docs/workflows/ios/selfhosted-on-demand-build.md | 1 + .github/docs/workflows/ios/selfhosted-release.md | 1 + .github/docs/workflows/ios/selfhosted-test.md | 1 + .github/docs/workflows/kmp/cloud-detect-changes.md | 1 + .github/docs/workflows/kmp/combined-nightly-build.md | 1 + .github/docs/workflows/universal/cloud-backup.md | 1 + .github/docs/workflows/universal/selfhosted-backup.md | 1 + .github/docs/workflows/universal/workflows-lint.md | 1 + .github/scripts/templates/workflow.md.j2 | 1 + 19 files changed, 19 insertions(+) diff --git a/.github/docs/workflows/android/cloud-check.md b/.github/docs/workflows/android/cloud-check.md index 0bc943a..14dc3ee 100644 --- a/.github/docs/workflows/android/cloud-check.md +++ b/.github/docs/workflows/android/cloud-check.md @@ -3,6 +3,7 @@ # Android PR Check **Source:** [`workflows/android-cloud-check.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-check.yml) + **Runner:** `ubuntu-latest` *Android Pull Request Check* diff --git a/.github/docs/workflows/android/cloud-generate-baseline-profiles.md b/.github/docs/workflows/android/cloud-generate-baseline-profiles.md index fc1cc0a..057ca04 100644 --- a/.github/docs/workflows/android/cloud-generate-baseline-profiles.md +++ b/.github/docs/workflows/android/cloud-generate-baseline-profiles.md @@ -3,6 +3,7 @@ # Android Generate Baseline Profiles **Source:** [`workflows/android-cloud-generate-baseline-profiles.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-generate-baseline-profiles.yml) + **Runner:** `ubuntu-latest` *Generate baseline profiles* diff --git a/.github/docs/workflows/android/cloud-nightly-build.md b/.github/docs/workflows/android/cloud-nightly-build.md index a64f941..4052297 100644 --- a/.github/docs/workflows/android/cloud-nightly-build.md +++ b/.github/docs/workflows/android/cloud-nightly-build.md @@ -3,6 +3,7 @@ # Android Nightly Build **Source:** [`workflows/android-cloud-nightly-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-nightly-build.yml) + **Runner:** `ubuntu-latest` *Android nightly build* diff --git a/.github/docs/workflows/android/cloud-release-firebase.md b/.github/docs/workflows/android/cloud-release-firebase.md index 4f3a4de..2b81631 100644 --- a/.github/docs/workflows/android/cloud-release-firebase.md +++ b/.github/docs/workflows/android/cloud-release-firebase.md @@ -3,6 +3,7 @@ # Android Release (Firebase) **Source:** [`workflows/android-cloud-release-firebaseAppDistribution.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-release-firebaseAppDistribution.yml) + **Runner:** `ubuntu-latest` *Android Release to Firebase App Distribution* diff --git a/.github/docs/workflows/android/cloud-release-googleplay.md b/.github/docs/workflows/android/cloud-release-googleplay.md index d4f7055..cb1bd77 100644 --- a/.github/docs/workflows/android/cloud-release-googleplay.md +++ b/.github/docs/workflows/android/cloud-release-googleplay.md @@ -3,6 +3,7 @@ # Android Release (Google Play) **Source:** [`workflows/android-cloud-release-googlePlay.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-release-googlePlay.yml) + **Runner:** `ubuntu-latest` *Android Release to Google Play* diff --git a/.github/docs/workflows/ios-kmp/selfhosted-build.md b/.github/docs/workflows/ios-kmp/selfhosted-build.md index 588456e..43c1f0b 100644 --- a/.github/docs/workflows/ios-kmp/selfhosted-build.md +++ b/.github/docs/workflows/ios-kmp/selfhosted-build.md @@ -3,6 +3,7 @@ # iOS KMP Build **Source:** [`workflows/ios-kmp-selfhosted-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-kmp-selfhosted-build.yml) + **Runner:** `Self-hosted` *iOS KMP Self-hosted Build* diff --git a/.github/docs/workflows/ios-kmp/selfhosted-release.md b/.github/docs/workflows/ios-kmp/selfhosted-release.md index 37077d9..355cd28 100644 --- a/.github/docs/workflows/ios-kmp/selfhosted-release.md +++ b/.github/docs/workflows/ios-kmp/selfhosted-release.md @@ -3,6 +3,7 @@ # iOS KMP Release **Source:** [`workflows/ios-kmp-selfhosted-release.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-kmp-selfhosted-release.yml) + **Runner:** `Self-hosted` *iOS KMP Self-hosted Release* diff --git a/.github/docs/workflows/ios-kmp/selfhosted-test.md b/.github/docs/workflows/ios-kmp/selfhosted-test.md index f2e0cbc..6075652 100644 --- a/.github/docs/workflows/ios-kmp/selfhosted-test.md +++ b/.github/docs/workflows/ios-kmp/selfhosted-test.md @@ -3,6 +3,7 @@ # iOS KMP Test **Source:** [`workflows/ios-kmp-selfhosted-test.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-kmp-selfhosted-test.yml) + **Runner:** `Self-hosted` *iOS KMP Self-hosted Test* diff --git a/.github/docs/workflows/ios/selfhosted-build.md b/.github/docs/workflows/ios/selfhosted-build.md index 628a17d..31d0aa0 100644 --- a/.github/docs/workflows/ios/selfhosted-build.md +++ b/.github/docs/workflows/ios/selfhosted-build.md @@ -6,6 +6,7 @@ Use `ios-selfhosted-nightly-build` instead. **Source:** [`workflows/ios-selfhosted-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-build.yml) + **Runner:** `Self-hosted` *Deprecated Build (use ios-selfhosted-nightly-build)* diff --git a/.github/docs/workflows/ios/selfhosted-nightly-build.md b/.github/docs/workflows/ios/selfhosted-nightly-build.md index 7a403d8..322e40a 100644 --- a/.github/docs/workflows/ios/selfhosted-nightly-build.md +++ b/.github/docs/workflows/ios/selfhosted-nightly-build.md @@ -3,6 +3,7 @@ # iOS Nightly Build **Source:** [`workflows/ios-selfhosted-nightly-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-nightly-build.yml) + **Runner:** `Self-hosted` *iOS Self-hosted Nightly Build* diff --git a/.github/docs/workflows/ios/selfhosted-on-demand-build.md b/.github/docs/workflows/ios/selfhosted-on-demand-build.md index 0e0ee35..95dcb23 100644 --- a/.github/docs/workflows/ios/selfhosted-on-demand-build.md +++ b/.github/docs/workflows/ios/selfhosted-on-demand-build.md @@ -3,6 +3,7 @@ # iOS On-Demand Build **Source:** [`workflows/ios-selfhosted-on-demand-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-on-demand-build.yml) + **Runner:** `Self-hosted` *iOS Self-hosted On-Demand Build* diff --git a/.github/docs/workflows/ios/selfhosted-release.md b/.github/docs/workflows/ios/selfhosted-release.md index 50016ee..48f857f 100644 --- a/.github/docs/workflows/ios/selfhosted-release.md +++ b/.github/docs/workflows/ios/selfhosted-release.md @@ -3,6 +3,7 @@ # iOS Release **Source:** [`workflows/ios-selfhosted-release.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-release.yml) + **Runner:** `Self-hosted` *iOS Self-hosted Release* diff --git a/.github/docs/workflows/ios/selfhosted-test.md b/.github/docs/workflows/ios/selfhosted-test.md index d5cb9bf..be5069b 100644 --- a/.github/docs/workflows/ios/selfhosted-test.md +++ b/.github/docs/workflows/ios/selfhosted-test.md @@ -3,6 +3,7 @@ # iOS Test **Source:** [`workflows/ios-selfhosted-test.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-test.yml) + **Runner:** `Self-hosted` *iOS Self-hosted Test* diff --git a/.github/docs/workflows/kmp/cloud-detect-changes.md b/.github/docs/workflows/kmp/cloud-detect-changes.md index 255c7b4..511bb3d 100644 --- a/.github/docs/workflows/kmp/cloud-detect-changes.md +++ b/.github/docs/workflows/kmp/cloud-detect-changes.md @@ -3,6 +3,7 @@ # KMP Detect Changes **Source:** [`workflows/kmp-cloud-detect-changes.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/kmp-cloud-detect-changes.yml) + **Runner:** `ubuntu-latest` *Detect Changes* diff --git a/.github/docs/workflows/kmp/combined-nightly-build.md b/.github/docs/workflows/kmp/combined-nightly-build.md index 746ce0b..d708a4a 100644 --- a/.github/docs/workflows/kmp/combined-nightly-build.md +++ b/.github/docs/workflows/kmp/combined-nightly-build.md @@ -3,6 +3,7 @@ # KMP Combined Nightly Build **Source:** [`workflows/kmp-combined-nightly-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/kmp-combined-nightly-build.yml) + **Runner:** `Self-hosted + ubuntu-latest` *KMP nightly build* diff --git a/.github/docs/workflows/universal/cloud-backup.md b/.github/docs/workflows/universal/cloud-backup.md index a333986..5156945 100644 --- a/.github/docs/workflows/universal/cloud-backup.md +++ b/.github/docs/workflows/universal/cloud-backup.md @@ -3,6 +3,7 @@ # Cloud Backup **Source:** [`workflows/universal-cloud-backup.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/universal-cloud-backup.yml) + **Runner:** `ubuntu-latest` *Backup* diff --git a/.github/docs/workflows/universal/selfhosted-backup.md b/.github/docs/workflows/universal/selfhosted-backup.md index c29ac19..9d83e2f 100644 --- a/.github/docs/workflows/universal/selfhosted-backup.md +++ b/.github/docs/workflows/universal/selfhosted-backup.md @@ -3,6 +3,7 @@ # Self-hosted Backup **Source:** [`workflows/universal-selfhosted-backup.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/universal-selfhosted-backup.yml) + **Runner:** `Self-hosted` *Backup* diff --git a/.github/docs/workflows/universal/workflows-lint.md b/.github/docs/workflows/universal/workflows-lint.md index c19a2f6..0ea57c0 100644 --- a/.github/docs/workflows/universal/workflows-lint.md +++ b/.github/docs/workflows/universal/workflows-lint.md @@ -6,6 +6,7 @@ This is not a reusable workflow — it runs directly on `pull_request` events in this repository. **Source:** [`workflows/workflows-lint.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/workflows-lint.yml) + **Runner:** `ubuntu-latest` *Check Pull Request* diff --git a/.github/scripts/templates/workflow.md.j2 b/.github/scripts/templates/workflow.md.j2 index 89d3210..e14f58f 100644 --- a/.github/scripts/templates/workflow.md.j2 +++ b/.github/scripts/templates/workflow.md.j2 @@ -14,6 +14,7 @@ {% endif %} **Source:** [`{{ source_path }}`](https://github.com/futuredapp/.github/blob/main/.github/{{ source_path }}) {% if runner %} + **Runner:** `{{ runner }}` {% endif %} From 8829e4958725bac2f41f376010a93453ec2e1261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20S=CC=8Cesta=CC=81k?= Date: Fri, 20 Feb 2026 12:01:47 +0000 Subject: [PATCH 04/10] chore: Gitignore generated docs (workflows/ and actions/) Generated markdown in docs/workflows/ and docs/actions/ can be regenerated with `python scripts/generate-docs.py`. Hand-crafted assets, stylesheets, and docs/index.md remain tracked. Co-Authored-By: Claude Opus 4.6 --- .github/.gitignore | 3 + .../docs/actions/android/build-firebase.md | 36 ----- .../docs/actions/android/build-googleplay.md | 41 ----- .github/docs/actions/android/check.md | 25 --- .../android/generate-baseline-profiles.md | 30 ---- .github/docs/actions/android/index.md | 14 -- .../docs/actions/android/setup-environment.md | 26 --- .github/docs/actions/index.md | 12 -- .github/docs/actions/ios/export-secrets.md | 24 --- .github/docs/actions/ios/fastlane-beta.md | 32 ---- .github/docs/actions/ios/fastlane-release.md | 32 ---- .github/docs/actions/ios/fastlane-test.md | 23 --- .github/docs/actions/ios/index.md | 14 -- .github/docs/actions/ios/kmp-build.md | 37 ----- .../utility/detect-changes-changelog.md | 148 ------------------ .github/docs/actions/utility/index.md | 12 -- .../utility/jira-transition-tickets.md | 120 -------------- .../actions/utility/kmp-detect-changes.md | 27 ---- .github/docs/workflows/android/cloud-check.md | 45 ------ .../cloud-generate-baseline-profiles.md | 51 ------ .../workflows/android/cloud-nightly-build.md | 63 -------- .../android/cloud-release-firebase.md | 57 ------- .../android/cloud-release-googleplay.md | 61 -------- .github/docs/workflows/android/index.md | 14 -- .github/docs/workflows/index.md | 14 -- .github/docs/workflows/ios-kmp/index.md | 12 -- .../workflows/ios-kmp/selfhosted-build.md | 61 -------- .../workflows/ios-kmp/selfhosted-release.md | 58 ------- .../docs/workflows/ios-kmp/selfhosted-test.md | 43 ----- .github/docs/workflows/ios/index.md | 14 -- .../docs/workflows/ios/selfhosted-build.md | 53 ------- .../workflows/ios/selfhosted-nightly-build.md | 55 ------- .../ios/selfhosted-on-demand-build.md | 53 ------- .../docs/workflows/ios/selfhosted-release.md | 49 ------ .github/docs/workflows/ios/selfhosted-test.md | 37 ----- .../workflows/kmp/cloud-detect-changes.md | 35 ----- .../workflows/kmp/combined-nightly-build.md | 80 ---------- .github/docs/workflows/kmp/index.md | 11 -- .../docs/workflows/universal/cloud-backup.md | 38 ----- .github/docs/workflows/universal/index.md | 12 -- .../workflows/universal/selfhosted-backup.md | 38 ----- .../workflows/universal/workflows-lint.md | 13 -- 42 files changed, 3 insertions(+), 1620 deletions(-) create mode 100644 .github/.gitignore delete mode 100644 .github/docs/actions/android/build-firebase.md delete mode 100644 .github/docs/actions/android/build-googleplay.md delete mode 100644 .github/docs/actions/android/check.md delete mode 100644 .github/docs/actions/android/generate-baseline-profiles.md delete mode 100644 .github/docs/actions/android/index.md delete mode 100644 .github/docs/actions/android/setup-environment.md delete mode 100644 .github/docs/actions/index.md delete mode 100644 .github/docs/actions/ios/export-secrets.md delete mode 100644 .github/docs/actions/ios/fastlane-beta.md delete mode 100644 .github/docs/actions/ios/fastlane-release.md delete mode 100644 .github/docs/actions/ios/fastlane-test.md delete mode 100644 .github/docs/actions/ios/index.md delete mode 100644 .github/docs/actions/ios/kmp-build.md delete mode 100644 .github/docs/actions/utility/detect-changes-changelog.md delete mode 100644 .github/docs/actions/utility/index.md delete mode 100644 .github/docs/actions/utility/jira-transition-tickets.md delete mode 100644 .github/docs/actions/utility/kmp-detect-changes.md delete mode 100644 .github/docs/workflows/android/cloud-check.md delete mode 100644 .github/docs/workflows/android/cloud-generate-baseline-profiles.md delete mode 100644 .github/docs/workflows/android/cloud-nightly-build.md delete mode 100644 .github/docs/workflows/android/cloud-release-firebase.md delete mode 100644 .github/docs/workflows/android/cloud-release-googleplay.md delete mode 100644 .github/docs/workflows/android/index.md delete mode 100644 .github/docs/workflows/index.md delete mode 100644 .github/docs/workflows/ios-kmp/index.md delete mode 100644 .github/docs/workflows/ios-kmp/selfhosted-build.md delete mode 100644 .github/docs/workflows/ios-kmp/selfhosted-release.md delete mode 100644 .github/docs/workflows/ios-kmp/selfhosted-test.md delete mode 100644 .github/docs/workflows/ios/index.md delete mode 100644 .github/docs/workflows/ios/selfhosted-build.md delete mode 100644 .github/docs/workflows/ios/selfhosted-nightly-build.md delete mode 100644 .github/docs/workflows/ios/selfhosted-on-demand-build.md delete mode 100644 .github/docs/workflows/ios/selfhosted-release.md delete mode 100644 .github/docs/workflows/ios/selfhosted-test.md delete mode 100644 .github/docs/workflows/kmp/cloud-detect-changes.md delete mode 100644 .github/docs/workflows/kmp/combined-nightly-build.md delete mode 100644 .github/docs/workflows/kmp/index.md delete mode 100644 .github/docs/workflows/universal/cloud-backup.md delete mode 100644 .github/docs/workflows/universal/index.md delete mode 100644 .github/docs/workflows/universal/selfhosted-backup.md delete mode 100644 .github/docs/workflows/universal/workflows-lint.md diff --git a/.github/.gitignore b/.github/.gitignore new file mode 100644 index 0000000..690ed16 --- /dev/null +++ b/.github/.gitignore @@ -0,0 +1,3 @@ +# Generated documentation (regenerate with: python scripts/generate-docs.py) +docs/workflows/ +docs/actions/ diff --git a/.github/docs/actions/android/build-firebase.md b/.github/docs/actions/android/build-firebase.md deleted file mode 100644 index 73bcf10..0000000 --- a/.github/docs/actions/android/build-firebase.md +++ /dev/null @@ -1,36 +0,0 @@ - - -# Build Firebase - -**Source:** [`actions/android-build-firebase/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/android-build-firebase/action.yml) - -Builds and uploads app to Firebase App Distribution. - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/android-build-firebase@main - with: - test_gradle_task: '...' - package_gradle_task: '...' - upload_gradle_task: '...' - app_distribution_groups: '...' - app_distribution_service_account: '...' -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `test_gradle_task` | `string` | Yes | — | A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest` | -| `package_gradle_task` | `string` | Yes | — | A Gradle task for packaging universal APK, eg. 'packageEnterpriseUniversalApk' | -| `upload_gradle_task` | `string` | Yes | — | A Gradle task for uploading APK, for example `appDistributionUploadEnterprise` | -| `app_distribution_groups` | `string` | Yes | — | Comma-separated list of Firebase App Distribution group IDs | -| `app_distribution_service_account` | `string` | Yes | — | JSON key of service account with permissions to upload build to Firebase App Distribution | -| `version_name` | `string` | No | — | Version name. Example: '1.X.X-snapshot' | -| `build_number_offset` | `string` | No | `0` | Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers. | -| `release_notes` | `string` | No | `${{ github.event.head_commit.message }}` | Release notes for this build | -| `kmp_flavor` | `string` | No | `test` | KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects | -| `secret_properties_file` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | -| `secret_properties` | `string` | No | — | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | - diff --git a/.github/docs/actions/android/build-googleplay.md b/.github/docs/actions/android/build-googleplay.md deleted file mode 100644 index 95a71ac..0000000 --- a/.github/docs/actions/android/build-googleplay.md +++ /dev/null @@ -1,41 +0,0 @@ - - -# Build Google Play - -**Source:** [`actions/android-build-googlePlay/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/android-build-googlePlay/action.yml) - -Builds and uploads app to Google Play. - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/android-build-googlePlay@main - with: - bundle_gradle_task: '...' - version_name: '...' - signing_keystore_password: '...' - signing_key_alias: '...' - signing_key_password: '...' - google_play_application_id: '...' - google_play_whatsnew_dir: '...' - google_play_publish_service_account: '...' -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `bundle_gradle_task` | `string` | Yes | — | A Gradle task for assembling app bundle, for example `bundleRelease` | -| `version_name` | `string` | Yes | — | Version name. Example: '1.0.0' | -| `signing_keystore_password` | `string` | Yes | — | Password to provided keystore | -| `signing_key_alias` | `string` | Yes | — | Alias of the signing key in the provided keystore | -| `signing_key_password` | `string` | Yes | — | Password to the key in the provided keystore | -| `google_play_application_id` | `string` | Yes | — | Google Play applicationId | -| `google_play_whatsnew_dir` | `string` | Yes | — | Path to directory with changelog files according to documentation in https://github.com/r0adkll/upload-google-play | -| `google_play_publish_service_account` | `string` | Yes | — | JSON key of service account with permissions to upload build to Google Play | -| `changes_not_sent_for_review` | `string` | No | `False` | A changesNotSentForReview Google Play flag. Enable when last google review failed, disable when last review was successful. | -| `build_number_offset` | `string` | No | `0` | Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers. | -| `kmp_flavor` | `string` | No | `prod` | KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects | -| `secret_properties_file` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | -| `secret_properties` | `string` | No | — | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | - diff --git a/.github/docs/actions/android/check.md b/.github/docs/actions/android/check.md deleted file mode 100644 index cab7809..0000000 --- a/.github/docs/actions/android/check.md +++ /dev/null @@ -1,25 +0,0 @@ - - -# Android Check - -**Source:** [`actions/android-check/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/android-check/action.yml) - -Runs lint checks and unit tests. - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/android-check@main - with: - lint_gradle_task: '...' - test_gradle_task: '...' -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `lint_gradle_task` | `string` | Yes | — | A Gradle task(s) for executing lint check, for example `lintCheck lintRelease` | -| `test_gradle_task` | `string` | Yes | — | A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest` | -| `github_token_danger` | `string` | No | — | GitHub token for Danger. Must have permissions to read and write issues and pull requests. | - diff --git a/.github/docs/actions/android/generate-baseline-profiles.md b/.github/docs/actions/android/generate-baseline-profiles.md deleted file mode 100644 index c6f3898..0000000 --- a/.github/docs/actions/android/generate-baseline-profiles.md +++ /dev/null @@ -1,30 +0,0 @@ - - -# Generate Baseline Profiles - -**Source:** [`actions/android-generate-baseline-profiles/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/android-generate-baseline-profiles/action.yml) - -Generates baseline profiles and creates a PR with the changes - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/android-generate-baseline-profiles@main - with: - generate_gradle_task: '...' - signing_keystore_password: '...' - signing_key_alias: '...' - signing_key_password: '...' -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `generate_gradle_task` | `string` | Yes | — | A Gradle task for generating baseline profiles, for example `generateBaselineProfile` | -| `signing_keystore_password` | `string` | Yes | — | Password to provided keystore | -| `signing_key_alias` | `string` | Yes | — | Alias of the signing key in the provided keystore | -| `signing_key_password` | `string` | Yes | — | Password to the key in the provided keystore | -| `secret_properties_file` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | -| `secret_properties` | `string` | No | — | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | - diff --git a/.github/docs/actions/android/index.md b/.github/docs/actions/android/index.md deleted file mode 100644 index 361e8e4..0000000 --- a/.github/docs/actions/android/index.md +++ /dev/null @@ -1,14 +0,0 @@ - - -# Android Actions - -Composite GitHub Actions for Android projects. - -| Name | Description | -|------|-------------| -| [Setup Environment](setup-environment.md) | Sets up Java, Gradle and Ruby and other preconditions for CI runs at Futured Android workflows. | -| [Android Check](check.md) | Runs lint checks and unit tests. | -| [Build Firebase](build-firebase.md) | Builds and uploads app to Firebase App Distribution. | -| [Build Google Play](build-googleplay.md) | Builds and uploads app to Google Play. | -| [Generate Baseline Profiles](generate-baseline-profiles.md) | Generates baseline profiles and creates a PR with the changes | - diff --git a/.github/docs/actions/android/setup-environment.md b/.github/docs/actions/android/setup-environment.md deleted file mode 100644 index 056c815..0000000 --- a/.github/docs/actions/android/setup-environment.md +++ /dev/null @@ -1,26 +0,0 @@ - - -# Setup Environment - -**Source:** [`actions/android-setup-environment/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/android-setup-environment/action.yml) - -Sets up Java, Gradle and Ruby and other preconditions for CI runs at Futured Android workflows. - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/android-setup-environment@main -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `java` | `string` | No | `true` | Whether to set up Java | -| `java_version` | `string` | No | `17` | Java version to use, eg. '17'. | -| `java_distribution` | `string` | No | `zulu` | Java distribution to use, eg 'zulu'. | -| `ruby` | `string` | No | `true` | Whether to set up Ruby | -| `ruby_version` | `string` | No | `3.4` | Ruby version. | -| `gradle` | `string` | No | `true` | Whether to set up Gradle | -| `gradle_cache_encryption_key` | `string` | No | — | Configuration cache encryption key. Leave empty if you don't need cache. | - diff --git a/.github/docs/actions/index.md b/.github/docs/actions/index.md deleted file mode 100644 index 1a9ba17..0000000 --- a/.github/docs/actions/index.md +++ /dev/null @@ -1,12 +0,0 @@ - - -# Actions - -All composite GitHub Actions organized by platform. - -| Name | Description | -|------|-------------| -| [iOS Actions](ios/index.md) | 5 action(s) | -| [Android Actions](android/index.md) | 5 action(s) | -| [Utility Actions](utility/index.md) | 3 action(s) | - diff --git a/.github/docs/actions/ios/export-secrets.md b/.github/docs/actions/ios/export-secrets.md deleted file mode 100644 index 3dbc8f6..0000000 --- a/.github/docs/actions/ios/export-secrets.md +++ /dev/null @@ -1,24 +0,0 @@ - - -# Export Secrets - -**Source:** [`actions/ios-export-secrets/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/ios-export-secrets/action.yml) - -Encodes secret values to Base64 and writes them into the specified .xcconfig file in KEY = VALUE format. - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/ios-export-secrets@main - with: - XCCONFIG_PATH: '...' -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `XCCONFIG_PATH` | `string` | Yes | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | -| `SECRET_PROPERTIES` | `string` | No | — | Secrets in the format KEY = VALUE (one per line). | -| `REQUIRED_KEYS` | `string` | No | — | Comma-separated list of required keys. | - diff --git a/.github/docs/actions/ios/fastlane-beta.md b/.github/docs/actions/ios/fastlane-beta.md deleted file mode 100644 index fe25427..0000000 --- a/.github/docs/actions/ios/fastlane-beta.md +++ /dev/null @@ -1,32 +0,0 @@ - - -# Fastlane Beta - -**Source:** [`actions/ios-fastlane-beta/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/ios-fastlane-beta/action.yml) - -Runs Fastlane beta - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/ios-fastlane-beta@main - with: - match_password: '...' - app_store_connect_api_key_key: '...' - app_store_connect_api_key_key_id: '...' - app_store_connect_api_key_issuer_id: '...' -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `match_password` | `string` | Yes | — | Match password | -| `testflight_changelog` | `string` | No | — | Testflight changelog | -| `app_store_connect_api_key_key` | `string` | Yes | — | App Store Connect API Key | -| `app_store_connect_api_key_key_id` | `string` | Yes | — | App Store Connect API Key ID | -| `app_store_connect_api_key_issuer_id` | `string` | Yes | — | App Store Connect API Key Issuer ID | -| `custom_values` | `string` | No | — | Custom values | -| `ios_root_path` | `string` | No | — | Path to iOS project root directory containing Gemfile. If not specified, uses current directory. | -| `custom_build_path` | `string` | No | — | Path to directory containing Fastfile. If not specified, uses ios_root_path or current directory. | - diff --git a/.github/docs/actions/ios/fastlane-release.md b/.github/docs/actions/ios/fastlane-release.md deleted file mode 100644 index 4dcb963..0000000 --- a/.github/docs/actions/ios/fastlane-release.md +++ /dev/null @@ -1,32 +0,0 @@ - - -# Fastlane Release - -**Source:** [`actions/ios-fastlane-release/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/ios-fastlane-release/action.yml) - -Runs Fastlane release - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/ios-fastlane-release@main - with: - match_password: '...' - app_store_connect_api_key_key: '...' - app_store_connect_api_key_key_id: '...' - app_store_connect_api_key_issuer_id: '...' -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `match_password` | `string` | Yes | — | Match password | -| `version_number` | `string` | No | — | Version number | -| `app_store_connect_api_key_key` | `string` | Yes | — | App Store Connect API Key | -| `app_store_connect_api_key_key_id` | `string` | Yes | — | App Store Connect API Key ID | -| `app_store_connect_api_key_issuer_id` | `string` | Yes | — | App Store Connect API Key Issuer ID | -| `custom_values` | `string` | No | — | Custom values | -| `ios_root_path` | `string` | No | — | Path to iOS project root directory containing Gemfile. If not specified, uses current directory. | -| `custom_build_path` | `string` | No | — | Path to directory containing Fastfile. If not specified, uses ios_root_path or current directory. | - diff --git a/.github/docs/actions/ios/fastlane-test.md b/.github/docs/actions/ios/fastlane-test.md deleted file mode 100644 index 0c20f2e..0000000 --- a/.github/docs/actions/ios/fastlane-test.md +++ /dev/null @@ -1,23 +0,0 @@ - - -# Fastlane Test - -**Source:** [`actions/ios-fastlane-test/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/ios-fastlane-test/action.yml) - -Runs Fastlane test - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/ios-fastlane-test@main -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `github_token` | `string` | No | — | GitHub token | -| `custom_values` | `string` | No | — | Custom values | -| `ios_root_path` | `string` | No | — | Path to iOS project root directory containing Gemfile. If not specified, uses current directory. | -| `custom_build_path` | `string` | No | — | Path to directory containing Fastfile. If not specified, uses ios_root_path or current directory. | - diff --git a/.github/docs/actions/ios/index.md b/.github/docs/actions/ios/index.md deleted file mode 100644 index 15c1bc9..0000000 --- a/.github/docs/actions/ios/index.md +++ /dev/null @@ -1,14 +0,0 @@ - - -# iOS Actions - -Composite GitHub Actions for iOS projects. - -| Name | Description | -|------|-------------| -| [Export Secrets](export-secrets.md) | Encodes secret values to Base64 and writes them into the specified .xcconfig file in KEY = VALUE format. | -| [Fastlane Test](fastlane-test.md) | Runs Fastlane test | -| [Fastlane Beta](fastlane-beta.md) | Runs Fastlane beta | -| [Fastlane Release](fastlane-release.md) | Runs Fastlane release | -| [KMP Build](kmp-build.md) | Builds iOS app (optionally with KMP framework) and uploads to TestFlight | - diff --git a/.github/docs/actions/ios/kmp-build.md b/.github/docs/actions/ios/kmp-build.md deleted file mode 100644 index e02ba58..0000000 --- a/.github/docs/actions/ios/kmp-build.md +++ /dev/null @@ -1,37 +0,0 @@ - - -# KMP Build - -**Source:** [`actions/ios-kmp-build/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/ios-kmp-build/action.yml) - -Builds iOS app (optionally with KMP framework) and uploads to TestFlight - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/ios-kmp-build@main - with: - match_password: '...' - app_store_connect_api_key_key: '...' - app_store_connect_api_key_key_id: '...' - app_store_connect_api_key_issuer_id: '...' -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `match_password` | `string` | Yes | — | Password for decrypting of certificates and provisioning profiles. | -| `app_store_connect_api_key_key` | `string` | Yes | — | Private App Store Connect API key for submitting build to App Store. | -| `app_store_connect_api_key_key_id` | `string` | Yes | — | Private App Store Connect API key for submitting build to App Store. | -| `app_store_connect_api_key_issuer_id` | `string` | Yes | — | Private App Store Connect API issuer key for submitting build to App Store. | -| `secret_xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | -| `secret_properties` | `string` | No | — | Secrets in the format KEY = VALUE (one per line). | -| `secret_required_keys` | `string` | No | — | Comma-separated list of required secret keys. | -| `kmp_swift_package_integration` | `string` | No | — | Whether KMP is integrated in Xcode project as a Swift Package | -| `kmp_swift_package_path` | `string` | No | — | If `kmp_swift_package_integration` is 'true', then specifies a location of local Swift Package with Makefile. Example: 'iosApp/shared/KMP` | -| `kmp_swift_package_flavor` | `string` | No | — | If `kmp_swift_package_integration`, specifies build flavor of KMP Package | -| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | -| `testflight_changelog` | `string` | No | — | Will be used as TestFlight changelog | -| `ios_custom_build_path` | `string` | No | — | Path to the directory containing Fastfile. If not specified, uses iosApp. Example: iosApp/appA | - diff --git a/.github/docs/actions/utility/detect-changes-changelog.md b/.github/docs/actions/utility/detect-changes-changelog.md deleted file mode 100644 index 348a8bc..0000000 --- a/.github/docs/actions/utility/detect-changes-changelog.md +++ /dev/null @@ -1,148 +0,0 @@ - - -# Detect Changes & Changelog - -**Source:** [`actions/universal-detect-changes-and-generate-changelog/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/universal-detect-changes-and-generate-changelog/action.yml) - -Detects changes since the last built commit and generates a changelog. - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/universal-detect-changes-and-generate-changelog@main -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `checkout_depth` | `number` | No | `100` | The depth of the git history to fetch. Default is 100. | -| `debug` | `boolean` | No | `False` | Enable debug mode. Default is false. | -| `fallback_lookback` | `string` | No | `24 hours` | The amount of time to look back for merge commits when no previous build commit is found (e.g., "24 hours", "7 days", "2 weeks"). Default is "24 hours". | -| `cache_key_prefix` | `string` | No | — | Custom prefix for cache keys. If not provided, will use latest_builded_commit-. If provided, format will be {prefix}-latest_builded_commit- | -| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files during checkout. Default is false. | -| `exclude_source_branches` | `string` | No | `(main|develop|master)` | Exclude merged commits of given branches. Regex pattern (ERE). Example: "(release.*|hotfix.*)" | - -## Outputs - -| Name | Description | -|------|-------------| -| `skip_build` | Indicates if the build should be skipped | -| `changelog` | The generated changelog formatted as a string | -| `merged_branches` | List of merged branch names | -| `cache_key` | Cache key to store latest built commit for this branch | - -## Additional Details - -## Features - -- ✅ **Nested Merge Detection**: Detects ALL merged branches including nested merges (e.g., B→A→develop reports both A and B) -- ✅ **Smart Filtering**: Automatically filters out reverse merges (main→feature) used for conflict resolution -- ✅ **Modular Design**: Split into separate bash scripts for better maintainability -- ✅ **Customizable Cache Keys**: Support for custom cache key prefixes (format: `latest_builded_commit-` or `{prefix}-latest_builded_commit-`) -- ✅ **GitHub-Native**: Leverages GitHub's built-in branch handling (no manual sanitization) -- ✅ **Comprehensive Testing**: Unit tests for all bash scripts -- ✅ **Debug Support**: Detailed debug output when enabled - -## Nested Merge Detection - -The action detects **all merged branches** including nested merges where one feature branch merges into another before merging to main. - -### How It Works - -**Example:** Branch B merges into branch A, then A merges into develop -- **Output:** Both `feature-A` and `feature-B` are detected -- **Filtering:** Reverse merges (e.g., `develop→feature-A` for conflict resolution) are automatically excluded - -### Implementation Details - -- **Branch Names**: Uses `git log --merges` (without `--first-parent`) to see all merge commits -- **Changelog Messages**: Uses `git log --merges --first-parent` to follow only main branch history -- **Filtering**: Excludes source branches via `grep -v "Merge branch '(EXCLUDE_SOURCE_BRANCHES)' into"` - -### Performance Considerations - -Removing `--first-parent` for branch detection means git traverses more of the commit graph: -- **Impact**: Minimal for typical workflows (10-100 commits between builds) -- **Large histories**: May add 1-2 seconds for repos with 1000+ commits in the range -- **Recommendation**: Use `checkout_depth` to limit git history fetch depth if needed - -The performance tradeoff is generally acceptable given the improved accuracy in branch detection. - -## Scripts - -### `cache-keys.sh` -Handles cache key calculation. - -**Environment Variables:** -- `CACHE_KEY_PREFIX`: Custom cache key prefix -- `DEBUG`: Debug mode flag - -**Outputs:** -- `cache_key_prefix`: Generated cache key prefix (format: `latest_builded_commit-` or `{prefix}-latest_builded_commit-`) - -### `determine-range.sh` -Determines commit range and skip build logic. - -**Environment Variables:** -- `DEBUG`: Debug mode flag -- `FALLBACK_LOOKBACK`: Fallback time window - -**Outputs:** -- `build_should_skip`: Whether to skip the build -- `from_commit`: Starting commit for changelog -- `to_commit`: Ending commit for changelog - -### `generate-changelog.sh` -Generates formatted changelog and branch names with nested merge detection. - -**Environment Variables:** -- `FROM_COMMIT`: Starting commit -- `TO_COMMIT`: Ending commit -- `EXCLUDE_SOURCE_BRANCHES`: Regex pattern for excluding source branches (default: `(main|develop|master)`) -- `DEBUG`: Debug mode flag - -**Outputs:** -- `changelog_string`: Formatted changelog (from main branch history only) -- `merged_branches`: List of all merged branches (includes nested merges) - -## Testing - -The action includes comprehensive unit tests using BATS (Bash Automated Testing System) and automated CI testing. - -### Running Tests - -```bash -# Run all tests -./test/run_tests.sh - -# Run specific test file -bats test/test_cache-keys.bats -bats test/test_determine-range.bats -bats test/test_generate-changelog.bats -bats test/test_merged-branches.bats -``` - -### CI Testing - -Tests run automatically on pull requests when relevant files change. The CI workflow includes: -- Unit tests for all bash scripts -- YAML syntax validation -- Concurrency cancellation (new commits cancel old tests) - -### Test Coverage - -- ✅ Cache key generation with custom prefixes -- ✅ Branch name detection and fallback logic -- ✅ Commit range determination logic -- ✅ Skip build decision making -- ✅ Changelog generation and formatting -- ✅ **Nested merge detection** (B→A→develop, C→B→A→develop) -- ✅ **Reverse merge filtering** (conflict resolution exclusion) -- ✅ **Custom target branch patterns** -- ✅ Error handling and edge cases -- ✅ Debug output functionality -- ✅ Git command failure scenarios -- ✅ Empty input handling -- ✅ Special characters and edge cases - diff --git a/.github/docs/actions/utility/index.md b/.github/docs/actions/utility/index.md deleted file mode 100644 index 8725fd6..0000000 --- a/.github/docs/actions/utility/index.md +++ /dev/null @@ -1,12 +0,0 @@ - - -# Utility Actions - -Composite GitHub Actions for Utility projects. - -| Name | Description | -|------|-------------| -| [KMP Detect Changes](kmp-detect-changes.md) | Detects changes in KMP project to determine which platform-specific workflows should run | -| [Detect Changes & Changelog](detect-changes-changelog.md) | Detects changes since the last built commit and generates a changelog. | -| [JIRA Transition Tickets](jira-transition-tickets.md) | Finds and transitions JIRA tickets based on branch names. | - diff --git a/.github/docs/actions/utility/jira-transition-tickets.md b/.github/docs/actions/utility/jira-transition-tickets.md deleted file mode 100644 index 8705235..0000000 --- a/.github/docs/actions/utility/jira-transition-tickets.md +++ /dev/null @@ -1,120 +0,0 @@ - - -# JIRA Transition Tickets - -**Source:** [`actions/jira-transition-tickets/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/jira-transition-tickets/action.yml) - -Finds and transitions JIRA tickets based on branch names. - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/jira-transition-tickets@main - with: - jira_context: '...' - transition: '...' - merged_branches: '...' -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `jira_context` | `string` | Yes | — | A base64-encoded string of Jira context object. See README for required structure. | -| `transition` | `string` | Yes | — | The name of the transition to transition the JIRA tickets. | -| `merged_branches` | `string` | Yes | — | A comma-separated string of merged branch names. | - -## Additional Details - -### `jira_context` (required) - -A base64-encoded JSON string containing JIRA authentication credentials and configuration. - -**Structure:** -```json -{ - "cloud_id": "your-cloud-id", - "user_email": "your-bot@serviceaccount.atlassian.com", - "api_token": "YourJiraApiToken" -} -``` - -**How to obtain Cloud ID:** - -Navigate to [https://.atlassian.net/_edge/tenant_info](https://.atlassian.net/_edge/tenant_info) - -**How to encode:** -```bash -echo -n '{"cloud_id":"your-cloud-id","user_email":"bot@example.com","api_token":"token"}' | base64 -``` - -**GitHub Secrets:** -Store the base64-encoded string in a GitHub secret (e.g., `JIRA_CONTEXT`) for secure usage. - -### `transition` (required) - -The name of the JIRA transition to execute. This must match the exact transition name in your JIRA workflow. - -**Examples:** `"Done"`, `"In QA"`, `"Ready for Testing"`, `"Closed"` - -### `merged_branches` (required) - -A comma-separated string of branch names from which to extract JIRA ticket keys. - -The action extracts keys matching the pattern `[A-Z]+-[0-9]+` from each branch name. - -**Example:** `"feature/ABC-123-login,bugfix/XYZ-456-fix-crash"` - -## How It Works - -1. **Extract JIRA Keys:** Parses branch names to extract ticket keys (e.g., `ABC-123`) -2. **Get Available Transitions:** For each issue key, fetches available transitions from JIRA API -3. **Find Target Transition:** Matches the target status name to find the corresponding transition ID -4. **Perform Transition:** Executes the transition for each issue to move it to the target status - -## Usage Examples - -### Example 1: Transition tickets from merged branches - -```yaml -- name: Transition JIRA tickets - uses: futuredapp/.github/.github/actions/jira-transition-tickets@main - with: - jira_context: ${{ secrets.JIRA_CONTEXT }} - transition: "Ready for Testing" - merged_branches: "feature/PROJ-123-new-feature,bugfix/PROJ-456-bug-fix" -``` - -### Example 2: Transition tickets from dynamic branch list - -```yaml -- name: Transition tickets - uses: futuredapp/.github/.github/actions/jira-transition-tickets@main - with: - jira_context: ${{ secrets.JIRA_CONTEXT }} - transition: "Ready for Testing" - merged_branches: ${{ steps.get_branches.outputs.branches }} -``` - -### Example 3: In a reusable workflow - -```yaml -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Build app - run: ./build.sh - - - name: Transition JIRA tickets on success - if: success() - uses: futuredapp/.github/.github/actions/jira-transition-tickets@main - with: - jira_context: ${{ secrets.JIRA_CONTEXT }} - transition: "Ready for Testing" - merged_branches: ${{ github.head_ref }} -``` - diff --git a/.github/docs/actions/utility/kmp-detect-changes.md b/.github/docs/actions/utility/kmp-detect-changes.md deleted file mode 100644 index 72dce69..0000000 --- a/.github/docs/actions/utility/kmp-detect-changes.md +++ /dev/null @@ -1,27 +0,0 @@ - - -# KMP Detect Changes - -**Source:** [`actions/kmp-detect-changes/action.yml`](https://github.com/futuredapp/.github/blob/main/.github/actions/kmp-detect-changes/action.yml) - -Detects changes in KMP project to determine which platform-specific workflows should run - -## Usage - -```yaml -- uses: futuredapp/.github/.github/actions/kmp-detect-changes@main -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | - -## Outputs - -| Name | Description | -|------|-------------| -| `iosFiles` | Whether files affecting iOS build changed (all files except those in androidApp/) | -| `androidFiles` | Whether files affecting Android build changed (all files except those in iosApp/) | - diff --git a/.github/docs/workflows/android/cloud-check.md b/.github/docs/workflows/android/cloud-check.md deleted file mode 100644 index 14dc3ee..0000000 --- a/.github/docs/workflows/android/cloud-check.md +++ /dev/null @@ -1,45 +0,0 @@ - - -# Android PR Check - -**Source:** [`workflows/android-cloud-check.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-check.yml) - -**Runner:** `ubuntu-latest` - -*Android Pull Request Check* - -## Usage - -```yaml -jobs: - android-pr-check: - uses: futuredapp/.github/.github/workflows/android-cloud-check.yml@main - with: - LINT_GRADLE_TASKS: '...' - TEST_GRADLE_TASKS: '...' -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `LINT_GRADLE_TASKS` | `string` | Yes | — | A Gradle task(s) for executing lint check, for example `lintCheck lintRelease` | -| `TEST_GRADLE_TASKS` | `string` | Yes | — | A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest` | -| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | -| `TIMEOUT_MINUTES` | `number` | No | `30` | Job timeout in minutes | -| `JAVA_VERSION` | `string` | No | `17` | Java version to use | -| `JAVA_DISTRIBUTION` | `string` | No | `zulu` | Java distribution to use | -| `GRADLE_OPTS` | `string` | No | — | Gradle options | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | -| `GITHUB_TOKEN_DANGER` | No | GitHub token for Danger. Must have permissions to read and write issues and pull requests. | - -## Internal Actions Used - -- [`Setup Environment`](../../actions/android/setup-environment.md) -- [`Android Check`](../../actions/android/check.md) - diff --git a/.github/docs/workflows/android/cloud-generate-baseline-profiles.md b/.github/docs/workflows/android/cloud-generate-baseline-profiles.md deleted file mode 100644 index 057ca04..0000000 --- a/.github/docs/workflows/android/cloud-generate-baseline-profiles.md +++ /dev/null @@ -1,51 +0,0 @@ - - -# Android Generate Baseline Profiles - -**Source:** [`workflows/android-cloud-generate-baseline-profiles.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-generate-baseline-profiles.yml) - -**Runner:** `ubuntu-latest` - -*Generate baseline profiles* - -## Usage - -```yaml -jobs: - android-generate-baseline-profiles: - uses: futuredapp/.github/.github/workflows/android-cloud-generate-baseline-profiles.yml@main - with: - TASK_NAME: '...' - secrets: - SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} - SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} - SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `TASK_NAME` | `string` | Yes | — | A Gradle task for executing baseline profiles, for example `generateBaselineProfile` | -| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | -| `SECRET_PROPERTIES_FILE` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | -| `TIMEOUT_MINUTES` | `number` | No | `60` | Job timeout in minutes | -| `JAVA_VERSION` | `string` | No | `17` | Java version to use | -| `JAVA_DISTRIBUTION` | `string` | No | `zulu` | Java distribution to use | -| `GRADLE_OPTS` | `string` | No | — | Gradle options | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `SIGNING_KEYSTORE_PASSWORD` | Yes | Password to provided keystore | -| `SIGNING_KEY_ALIAS` | Yes | Alias of the signing key in the provided keystore | -| `SIGNING_KEY_PASSWORD` | Yes | Password to the key in the provided keystore | -| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | -| `SECRET_PROPERTIES` | No | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | - -## Internal Actions Used - -- [`Setup Environment`](../../actions/android/setup-environment.md) -- [`Generate Baseline Profiles`](../../actions/android/generate-baseline-profiles.md) - diff --git a/.github/docs/workflows/android/cloud-nightly-build.md b/.github/docs/workflows/android/cloud-nightly-build.md deleted file mode 100644 index 4052297..0000000 --- a/.github/docs/workflows/android/cloud-nightly-build.md +++ /dev/null @@ -1,63 +0,0 @@ - - -# Android Nightly Build - -**Source:** [`workflows/android-cloud-nightly-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-nightly-build.yml) - -**Runner:** `ubuntu-latest` - -*Android nightly build* - -## Usage - -```yaml -jobs: - android-nightly-build: - uses: futuredapp/.github/.github/workflows/android-cloud-nightly-build.yml@main - with: - TEST_GRADLE_TASKS: '...' - PACKAGE_GRADLE_TASK: '...' - UPLOAD_GRADLE_TASK: '...' - APP_DISTRIBUTION_GROUPS: '...' - secrets: - APP_DISTRIBUTION_SERVICE_ACCOUNT: ${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `TEST_GRADLE_TASKS` | `string` | Yes | — | A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest` | -| `PACKAGE_GRADLE_TASK` | `string` | Yes | — | A Gradle task for packaging universal APK, eg. 'packageEnterpriseUniversalApk' | -| `UPLOAD_GRADLE_TASK` | `string` | Yes | — | A Gradle task for uploading APK, for example `appDistributionUploadEnterprise` | -| `APP_DISTRIBUTION_GROUPS` | `string` | Yes | — | Comma-separated list of app distribution group IDs | -| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | -| `VERSION_NAME` | `string` | No | — | Version name. Example: '1.X.X-snapshot' | -| `BUILD_NUMBER_OFFSET` | `number` | No | `0` | Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers. | -| `KMP_FLAVOR` | `string` | No | `test` | KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects | -| `SECRET_PROPERTIES_FILE` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | -| `JAVA_VERSION` | `string` | No | `17` | Java version to use | -| `JAVA_DISTRIBUTION` | `string` | No | `zulu` | Java distribution to use | -| `GRADLE_OPTS` | `string` | No | — | Gradle options | -| `CHANGELOG_DEBUG` | `boolean` | No | `False` | Enable debug mode for changelog generation. Default is false. | -| `CHANGELOG_CHECKOUT_DEPTH` | `number` | No | `100` | The depth of the git history to fetch for changelog generation. Default is 100. | -| `CHANGELOG_FALLBACK_LOOKBACK` | `string` | No | `24 hours` | The amount of time to look back for merge commits when no previous build commit is found. Default is 24 hours. | -| `TIMEOUT_MINUTES` | `number` | No | `30` | Job timeout in minutes | -| `JIRA_TRANSITION` | `string` | No | `Testing` | Jira transition to use for transitioning related issues after build | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `APP_DISTRIBUTION_SERVICE_ACCOUNT` | Yes | JSON key of service account with permissions to upload build to Firebase App Distribution | -| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | -| `SECRET_PROPERTIES` | No | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | -| `JIRA_CONTEXT` | No | JIRA context for transitioning tickets. | - -## Internal Actions Used - -- [`Detect Changes & Changelog`](../../actions/utility/detect-changes-changelog.md) -- [`Setup Environment`](../../actions/android/setup-environment.md) -- [`Build Firebase`](../../actions/android/build-firebase.md) -- [`JIRA Transition Tickets`](../../actions/utility/jira-transition-tickets.md) - diff --git a/.github/docs/workflows/android/cloud-release-firebase.md b/.github/docs/workflows/android/cloud-release-firebase.md deleted file mode 100644 index 2b81631..0000000 --- a/.github/docs/workflows/android/cloud-release-firebase.md +++ /dev/null @@ -1,57 +0,0 @@ - - -# Android Release (Firebase) - -**Source:** [`workflows/android-cloud-release-firebaseAppDistribution.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-release-firebaseAppDistribution.yml) - -**Runner:** `ubuntu-latest` - -*Android Release to Firebase App Distribution* - -## Usage - -```yaml -jobs: - android-release-firebase: - uses: futuredapp/.github/.github/workflows/android-cloud-release-firebaseAppDistribution.yml@main - with: - TEST_GRADLE_TASKS: '...' - PACKAGE_GRADLE_TASK: '...' - UPLOAD_GRADLE_TASK: '...' - APP_DISTRIBUTION_GROUPS: '...' - secrets: - APP_DISTRIBUTION_SERVICE_ACCOUNT: ${{ secrets.APP_DISTRIBUTION_SERVICE_ACCOUNT }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `TEST_GRADLE_TASKS` | `string` | Yes | — | A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest` | -| `PACKAGE_GRADLE_TASK` | `string` | Yes | — | A Gradle task for packaging universal APK, eg. 'packageEnterpriseUniversalApk' | -| `UPLOAD_GRADLE_TASK` | `string` | Yes | — | A Gradle task for uploading APK, for example `appDistributionUploadEnterprise` | -| `APP_DISTRIBUTION_GROUPS` | `string` | Yes | — | Comma-separated list of app distribution group IDs | -| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | -| `VERSION_NAME` | `string` | No | — | Version name. Example: '1.X.X-snapshot' | -| `BUILD_NUMBER_OFFSET` | `number` | No | `0` | Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers. | -| `RELEASE_NOTES` | `string` | No | `${{ github.event.head_commit.message }}` | Release notes for this build | -| `KMP_FLAVOR` | `string` | No | `test` | KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects | -| `SECRET_PROPERTIES_FILE` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | -| `JAVA_VERSION` | `string` | No | `17` | Java version to use | -| `JAVA_DISTRIBUTION` | `string` | No | `zulu` | Java distribution to use | -| `GRADLE_OPTS` | `string` | No | — | Gradle options | -| `TIMEOUT_MINUTES` | `number` | No | `30` | Job timeout in minutes | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `APP_DISTRIBUTION_SERVICE_ACCOUNT` | Yes | JSON key of service account with permissions to upload build to Firebase App Distribution | -| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | -| `SECRET_PROPERTIES` | No | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | - -## Internal Actions Used - -- [`Setup Environment`](../../actions/android/setup-environment.md) -- [`Build Firebase`](../../actions/android/build-firebase.md) - diff --git a/.github/docs/workflows/android/cloud-release-googleplay.md b/.github/docs/workflows/android/cloud-release-googleplay.md deleted file mode 100644 index cb1bd77..0000000 --- a/.github/docs/workflows/android/cloud-release-googleplay.md +++ /dev/null @@ -1,61 +0,0 @@ - - -# Android Release (Google Play) - -**Source:** [`workflows/android-cloud-release-googlePlay.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/android-cloud-release-googlePlay.yml) - -**Runner:** `ubuntu-latest` - -*Android Release to Google Play* - -## Usage - -```yaml -jobs: - android-release-google-play: - uses: futuredapp/.github/.github/workflows/android-cloud-release-googlePlay.yml@main - with: - VERSION_NAME: '...' - BUNDLE_GRADLE_TASK: '...' - GOOGLE_PLAY_APPLICATION_ID: '...' - GOOGLE_PLAY_WHATSNEW_DIRECTORY: '...' - secrets: - SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }} - SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} - SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }} - GOOGLE_PLAY_PUBLISH_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_PLAY_PUBLISH_SERVICE_ACCOUNT }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `VERSION_NAME` | `string` | Yes | — | Build version name | -| `BUNDLE_GRADLE_TASK` | `string` | Yes | — | A Gradle task for assembling app bundle, for example `bundleRelease` | -| `GOOGLE_PLAY_APPLICATION_ID` | `string` | Yes | — | Google Play applicationId | -| `GOOGLE_PLAY_WHATSNEW_DIRECTORY` | `string` | Yes | — | Path to directory with changelog files according to documentation in https://github.com/r0adkll/upload-google-play | -| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | -| `BUILD_NUMBER_OFFSET` | `number` | No | `0` | Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers. | -| `KMP_FLAVOR` | `string` | No | `prod` | KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects | -| `SECRET_PROPERTIES_FILE` | `string` | No | `secrets.properties` | A path to file that fill be populated with contents of 'SECRET_PROPERTIES' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | -| `CHANGES_NOT_SENT_FOR_REVIEW` | `boolean` | No | `False` | A changesNotSentForReview Google Play flag. Enable when last google review failed, disable when last review was successful. | -| `TIMEOUT_MINUTES` | `number` | No | `30` | Job timeout in minutes | -| `JAVA_VERSION` | `string` | No | `17` | Java version to use | -| `JAVA_DISTRIBUTION` | `string` | No | `zulu` | Java distribution to use | -| `GRADLE_OPTS` | `string` | No | — | Gradle options | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `SIGNING_KEYSTORE_PASSWORD` | Yes | Password to provided keystore | -| `SIGNING_KEY_ALIAS` | Yes | Alias of the signing key in the provided keystore | -| `SIGNING_KEY_PASSWORD` | Yes | Password to the key in the provided keystore | -| `GOOGLE_PLAY_PUBLISH_SERVICE_ACCOUNT` | Yes | JSON key of service account with permissions to upload build to Google Play | -| `SECRET_PROPERTIES` | No | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'SECRET_PROPERTIES_FILE' input. | - -## Internal Actions Used - -- [`Setup Environment`](../../actions/android/setup-environment.md) -- [`Build Google Play`](../../actions/android/build-googleplay.md) - diff --git a/.github/docs/workflows/android/index.md b/.github/docs/workflows/android/index.md deleted file mode 100644 index f7034a8..0000000 --- a/.github/docs/workflows/android/index.md +++ /dev/null @@ -1,14 +0,0 @@ - - -# Android Workflows - -Reusable GitHub Actions workflows for Android projects. - -| Name | Description | -|------|-------------| -| [Android PR Check](cloud-check.md) | Android Pull Request Check | -| [Android Nightly Build](cloud-nightly-build.md) | Android nightly build | -| [Android Release (Firebase)](cloud-release-firebase.md) | Android Release to Firebase App Distribution | -| [Android Release (Google Play)](cloud-release-googleplay.md) | Android Release to Google Play | -| [Android Generate Baseline Profiles](cloud-generate-baseline-profiles.md) | Generate baseline profiles | - diff --git a/.github/docs/workflows/index.md b/.github/docs/workflows/index.md deleted file mode 100644 index 8ebc4cb..0000000 --- a/.github/docs/workflows/index.md +++ /dev/null @@ -1,14 +0,0 @@ - - -# Workflows - -All reusable GitHub Actions workflows organized by platform. - -| Name | Description | -|------|-------------| -| [iOS Workflows](ios/index.md) | 5 workflow(s) | -| [iOS + KMP Workflows](ios-kmp/index.md) | 3 workflow(s) | -| [Android Workflows](android/index.md) | 5 workflow(s) | -| [KMP Workflows](kmp/index.md) | 2 workflow(s) | -| [Universal Workflows](universal/index.md) | 3 workflow(s) | - diff --git a/.github/docs/workflows/ios-kmp/index.md b/.github/docs/workflows/ios-kmp/index.md deleted file mode 100644 index 62564b6..0000000 --- a/.github/docs/workflows/ios-kmp/index.md +++ /dev/null @@ -1,12 +0,0 @@ - - -# iOS + KMP Workflows - -Reusable GitHub Actions workflows for iOS + KMP projects. - -| Name | Description | -|------|-------------| -| [iOS KMP Test](selfhosted-test.md) | iOS KMP Self-hosted Test | -| [iOS KMP Build](selfhosted-build.md) | iOS KMP Self-hosted Build | -| [iOS KMP Release](selfhosted-release.md) | iOS KMP Self-hosted Release | - diff --git a/.github/docs/workflows/ios-kmp/selfhosted-build.md b/.github/docs/workflows/ios-kmp/selfhosted-build.md deleted file mode 100644 index 43c1f0b..0000000 --- a/.github/docs/workflows/ios-kmp/selfhosted-build.md +++ /dev/null @@ -1,61 +0,0 @@ - - -# iOS KMP Build - -**Source:** [`workflows/ios-kmp-selfhosted-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-kmp-selfhosted-build.yml) - -**Runner:** `Self-hosted` - -*iOS KMP Self-hosted Build* - -## Usage - -```yaml -jobs: - ios-kmp-build: - uses: futuredapp/.github/.github/workflows/ios-kmp-selfhosted-build.yml@main - secrets: - MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} - APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} - APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} - APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | -| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | -| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | -| `kmp_swift_package_integration` | `boolean` | No | `False` | Whether KMP is integrated in Xcode project as a Swift Package | -| `kmp_swift_package_path` | `string` | No | `iosApp/shared/KMP` | If `swift_package_integration` is 'true', then specifies a location of local Swift Package with Makefile. Example: 'iosApp/shared/KMP` | -| `kmp_swift_package_flavor` | `string` | No | `prod` | Build flavor of KMP Package | -| `java_version` | `string` | No | `17` | Java version to use | -| `java_distribution` | `string` | No | `zulu` | Java distribution to use | -| `xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | -| `required_keys` | `string` | No | — | Comma-separated list of required keys. | -| `changelog` | `string` | No | `${{ github.event.pull_request.title }}` | Will be used as TestFlight changelog | -| `custom_build_path` | `string` | No | — | Path to directory containing Fastfile. If not specified, uses iosApp. Example: iosApp/appA | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. - | -| `APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. - | -| `APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. - | -| `APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. - | -| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | -| `SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). - | - -## Internal Actions Used - -- [`Setup Environment`](../../actions/android/setup-environment.md) -- [`KMP Build`](../../actions/ios/kmp-build.md) - diff --git a/.github/docs/workflows/ios-kmp/selfhosted-release.md b/.github/docs/workflows/ios-kmp/selfhosted-release.md deleted file mode 100644 index 355cd28..0000000 --- a/.github/docs/workflows/ios-kmp/selfhosted-release.md +++ /dev/null @@ -1,58 +0,0 @@ - - -# iOS KMP Release - -**Source:** [`workflows/ios-kmp-selfhosted-release.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-kmp-selfhosted-release.yml) - -**Runner:** `Self-hosted` - -*iOS KMP Self-hosted Release* - -## Usage - -```yaml -jobs: - ios-kmp-release: - uses: futuredapp/.github/.github/workflows/ios-kmp-selfhosted-release.yml@main - secrets: - MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} - APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} - APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} - APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | -| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | -| `kmp_swift_package_integration` | `boolean` | No | `False` | Whether KMP is integrated in Xcode project as a Swift Package | -| `kmp_swift_package_path` | `string` | No | `iosApp/shared/KMP` | If `swift_package_integration` is 'true', then specifies a location of local Swift Package with Makefile. Example: 'iosApp/shared/KMP` | -| `kmp_swift_package_flavor` | `string` | No | `prod` | Build flavor of KMP Package | -| `java_version` | `string` | No | `17` | Java version to use | -| `java_distribution` | `string` | No | `zulu` | Java distribution to use | -| `xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | -| `required_keys` | `string` | No | — | Comma-separated list of required keys. | -| `custom_build_path` | `string` | No | — | Path to directory containing Fastfile. If not specified, uses iosApp. Example: iosApp/appA | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. - | -| `APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. - | -| `APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. - | -| `APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. - | -| `SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). - | - -## Internal Actions Used - -- [`Export Secrets`](../../actions/ios/export-secrets.md) -- [`Fastlane Release`](../../actions/ios/fastlane-release.md) - diff --git a/.github/docs/workflows/ios-kmp/selfhosted-test.md b/.github/docs/workflows/ios-kmp/selfhosted-test.md deleted file mode 100644 index 6075652..0000000 --- a/.github/docs/workflows/ios-kmp/selfhosted-test.md +++ /dev/null @@ -1,43 +0,0 @@ - - -# iOS KMP Test - -**Source:** [`workflows/ios-kmp-selfhosted-test.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-kmp-selfhosted-test.yml) - -**Runner:** `Self-hosted` - -*iOS KMP Self-hosted Test* - -## Usage - -```yaml -jobs: - ios-kmp-test: - uses: futuredapp/.github/.github/workflows/ios-kmp-selfhosted-test.yml@main -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | -| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | -| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | -| `kmp_swift_package_integration` | `boolean` | No | `False` | Whether KMP is integrated in Xcode project as a Swift Package | -| `kmp_swift_package_path` | `string` | No | `iosApp/shared/KMP` | If `swift_package_integration` is 'true', then specifies a location of local Swift Package with Makefile. Example: 'iosApp/shared/KMP` | -| `kmp_swift_package_flavor` | `string` | No | `dev` | Build flavor of KMP Package | -| `java_version` | `string` | No | `17` | Java version to use | -| `java_distribution` | `string` | No | `zulu` | Java distribution to use | -| `custom_build_path` | `string` | No | — | Path to directory containing Fastfile. If not specified, uses iosApp. Example: iosApp/appA | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | -| `GITHUB_TOKEN_DANGER` | No | GitHub token for Danger. Must have permissions to read and write issues and pull requests. | - -## Internal Actions Used - -- [`Fastlane Test`](../../actions/ios/fastlane-test.md) - diff --git a/.github/docs/workflows/ios/index.md b/.github/docs/workflows/ios/index.md deleted file mode 100644 index 21a2dd9..0000000 --- a/.github/docs/workflows/ios/index.md +++ /dev/null @@ -1,14 +0,0 @@ - - -# iOS Workflows - -Reusable GitHub Actions workflows for iOS projects. - -| Name | Description | -|------|-------------| -| [iOS Test](selfhosted-test.md) | iOS Self-hosted Test | -| [iOS Nightly Build](selfhosted-nightly-build.md) | iOS Self-hosted Nightly Build | -| [iOS On-Demand Build](selfhosted-on-demand-build.md) | iOS Self-hosted On-Demand Build | -| [iOS Release](selfhosted-release.md) | iOS Self-hosted Release | -| [iOS Build (Deprecated)](selfhosted-build.md) | Deprecated Build (use ios-selfhosted-nightly-build) | - diff --git a/.github/docs/workflows/ios/selfhosted-build.md b/.github/docs/workflows/ios/selfhosted-build.md deleted file mode 100644 index 31d0aa0..0000000 --- a/.github/docs/workflows/ios/selfhosted-build.md +++ /dev/null @@ -1,53 +0,0 @@ - - -# iOS Build (Deprecated) - -!!! warning "Deprecated" - Use `ios-selfhosted-nightly-build` instead. - -**Source:** [`workflows/ios-selfhosted-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-build.yml) - -**Runner:** `Self-hosted` - -*Deprecated Build (use ios-selfhosted-nightly-build)* - -## Usage - -```yaml -jobs: - ios-build-deprecated: - uses: futuredapp/.github/.github/workflows/ios-selfhosted-build.yml@main - secrets: - MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} - APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} - APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} - APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | -| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | -| `runner_label` | `string` | No | `self-hosted` | The custom label for the self-hosted runner to use for the build job. | -| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | -| `xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | -| `secret_properties` | `string` | No | — | Secrets in the format KEY = VALUE (one per line). | -| `required_keys` | `string` | No | — | Comma-separated list of required keys. | -| `changelog_fallback_lookback` | `string` | No | `24 hours` | The amount of time to look back for merge commits when no previous build commit is found. Default is 24 hours. | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. | -| `APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. | -| `APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. | -| `APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. | -| `SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). | - -## Internal Actions Used - -- [`iOS Nightly Build`](selfhosted-nightly-build.md) - diff --git a/.github/docs/workflows/ios/selfhosted-nightly-build.md b/.github/docs/workflows/ios/selfhosted-nightly-build.md deleted file mode 100644 index 322e40a..0000000 --- a/.github/docs/workflows/ios/selfhosted-nightly-build.md +++ /dev/null @@ -1,55 +0,0 @@ - - -# iOS Nightly Build - -**Source:** [`workflows/ios-selfhosted-nightly-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-nightly-build.yml) - -**Runner:** `Self-hosted` - -*iOS Self-hosted Nightly Build* - -## Usage - -```yaml -jobs: - ios-nightly-build: - uses: futuredapp/.github/.github/workflows/ios-selfhosted-nightly-build.yml@main - secrets: - MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} - APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} - APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} - APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `runner_label` | `string` | No | `self-hosted` | The custom label for the self-hosted runner to use for the build job. | -| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | -| `checkout_depth` | `number` | No | `100` | The depth of the git history to fetch for changelog generation. | -| `changelog_fallback_lookback` | `string` | No | `24 hours` | The amount of time to look back for merge commits when no previous build commit is found. Default is 24 hours. | -| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | -| `xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | -| `required_keys` | `string` | No | — | Comma-separated list of required keys. | -| `custom_values` | `string` | No | `24 hours` | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | -| `jira_transition` | `string` | No | `Testing` | The name of the JIRA transition to apply to tickets found in merged branches. | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. | -| `APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. | -| `APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. | -| `APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. | -| `SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). | -| `JIRA_CONTEXT` | No | JIRA context for transitioning tickets. | - -## Internal Actions Used - -- [`Detect Changes & Changelog`](../../actions/utility/detect-changes-changelog.md) -- [`Export Secrets`](../../actions/ios/export-secrets.md) -- [`Fastlane Beta`](../../actions/ios/fastlane-beta.md) -- [`JIRA Transition Tickets`](../../actions/utility/jira-transition-tickets.md) - diff --git a/.github/docs/workflows/ios/selfhosted-on-demand-build.md b/.github/docs/workflows/ios/selfhosted-on-demand-build.md deleted file mode 100644 index 95dcb23..0000000 --- a/.github/docs/workflows/ios/selfhosted-on-demand-build.md +++ /dev/null @@ -1,53 +0,0 @@ - - -# iOS On-Demand Build - -**Source:** [`workflows/ios-selfhosted-on-demand-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-on-demand-build.yml) - -**Runner:** `Self-hosted` - -*iOS Self-hosted On-Demand Build* - -## Usage - -```yaml -jobs: - ios-on-demand-build: - uses: futuredapp/.github/.github/workflows/ios-selfhosted-on-demand-build.yml@main - secrets: - MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} - APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} - APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} - APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `runner_label` | `string` | No | `self-hosted` | The custom label for the self-hosted runner to use for the build job. | -| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | -| `changelog` | `string` | No | — | Will be used as TestFlight changelog | -| `checkout_depth` | `number` | No | `100` | The depth of the git history to fetch for changelog generation. | -| `changelog_fallback_lookback` | `string` | No | `24 hours` | The amount of time to look back for merge commits when no previous build commit is found. Default is 24 hours. | -| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | -| `xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | -| `required_keys` | `string` | No | — | Comma-separated list of required keys. | -| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. | -| `APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. | -| `APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. | -| `APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. | -| `SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). | - -## Internal Actions Used - -- [`Detect Changes & Changelog`](../../actions/utility/detect-changes-changelog.md) -- [`Export Secrets`](../../actions/ios/export-secrets.md) -- [`Fastlane Beta`](../../actions/ios/fastlane-beta.md) - diff --git a/.github/docs/workflows/ios/selfhosted-release.md b/.github/docs/workflows/ios/selfhosted-release.md deleted file mode 100644 index 48f857f..0000000 --- a/.github/docs/workflows/ios/selfhosted-release.md +++ /dev/null @@ -1,49 +0,0 @@ - - -# iOS Release - -**Source:** [`workflows/ios-selfhosted-release.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-release.yml) - -**Runner:** `Self-hosted` - -*iOS Self-hosted Release* - -## Usage - -```yaml -jobs: - ios-release: - uses: futuredapp/.github/.github/workflows/ios-selfhosted-release.yml@main - secrets: - MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }} - APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY }} - APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }} - APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `runner_label` | `string` | No | `self-hosted` | The custom label for the self-hosted runner to use for the build job. | -| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | -| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | -| `xcconfig_path` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | -| `required_keys` | `string` | No | — | Comma-separated list of required keys. | -| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. | -| `APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. | -| `APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. | -| `APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. | -| `SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). | - -## Internal Actions Used - -- [`Export Secrets`](../../actions/ios/export-secrets.md) -- [`Fastlane Release`](../../actions/ios/fastlane-release.md) - diff --git a/.github/docs/workflows/ios/selfhosted-test.md b/.github/docs/workflows/ios/selfhosted-test.md deleted file mode 100644 index be5069b..0000000 --- a/.github/docs/workflows/ios/selfhosted-test.md +++ /dev/null @@ -1,37 +0,0 @@ - - -# iOS Test - -**Source:** [`workflows/ios-selfhosted-test.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/ios-selfhosted-test.yml) - -**Runner:** `Self-hosted` - -*iOS Self-hosted Test* - -## Usage - -```yaml -jobs: - ios-test: - uses: futuredapp/.github/.github/workflows/ios-selfhosted-test.yml@main -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `use_git_lfs` | `boolean` | No | `False` | Whether to download Git-LFS files. | -| `custom_values` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | -| `runner_label` | `string` | No | `self-hosted` | The custom label for the self-hosted runner to use for the build job. | -| `timeout_minutes` | `number` | No | `30` | Job timeout in minutes | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `GITHUB_TOKEN_DANGER` | No | GitHub token for Danger. Must have permissions to read and write issues and pull requests. | - -## Internal Actions Used - -- [`Fastlane Test`](../../actions/ios/fastlane-test.md) - diff --git a/.github/docs/workflows/kmp/cloud-detect-changes.md b/.github/docs/workflows/kmp/cloud-detect-changes.md deleted file mode 100644 index 511bb3d..0000000 --- a/.github/docs/workflows/kmp/cloud-detect-changes.md +++ /dev/null @@ -1,35 +0,0 @@ - - -# KMP Detect Changes - -**Source:** [`workflows/kmp-cloud-detect-changes.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/kmp-cloud-detect-changes.yml) - -**Runner:** `ubuntu-latest` - -*Detect Changes* - -## Usage - -```yaml -jobs: - kmp-detect-changes: - uses: futuredapp/.github/.github/workflows/kmp-cloud-detect-changes.yml@main -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | - -## Outputs - -| Name | Description | -|------|-------------| -| `iosFiles` | Whether files affecting iOS build changed (all files except those in androidApp/) | -| `androidFiles` | Whether files affecting Android build changed (all files except those in iosApp/) | - -## Internal Actions Used - -- [`KMP Detect Changes`](../../actions/utility/kmp-detect-changes.md) - diff --git a/.github/docs/workflows/kmp/combined-nightly-build.md b/.github/docs/workflows/kmp/combined-nightly-build.md deleted file mode 100644 index d708a4a..0000000 --- a/.github/docs/workflows/kmp/combined-nightly-build.md +++ /dev/null @@ -1,80 +0,0 @@ - - -# KMP Combined Nightly Build - -**Source:** [`workflows/kmp-combined-nightly-build.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/kmp-combined-nightly-build.yml) - -**Runner:** `Self-hosted + ubuntu-latest` - -*KMP nightly build* - -## Usage - -```yaml -jobs: - kmp-combined-nightly-build: - uses: futuredapp/.github/.github/workflows/kmp-combined-nightly-build.yml@main - with: - ANDROID_TEST_GRADLE_TASK: '...' - ANDROID_PACKAGE_GRADLE_TASK: '...' - ANDROID_UPLOAD_GRADLE_TASK: '...' - KMP_FLAVOR: '...' - FIREBASE_APP_DISTRIBUTION_GROUPS: '...' - secrets: - FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT: ${{ secrets.FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT }} - IOS_MATCH_PASSWORD: ${{ secrets.IOS_MATCH_PASSWORD }} - IOS_APP_STORE_CONNECT_API_KEY_KEY: ${{ secrets.IOS_APP_STORE_CONNECT_API_KEY_KEY }} - IOS_APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.IOS_APP_STORE_CONNECT_API_KEY_KEY_ID }} - IOS_APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.IOS_APP_STORE_CONNECT_API_KEY_ISSUER_ID }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `ANDROID_TEST_GRADLE_TASK` | `string` | Yes | — | A Gradle task(s) for executing unit tests, for example `testReleaseUnitTest` or `testDevEnterpriseUnitTest` | -| `ANDROID_PACKAGE_GRADLE_TASK` | `string` | Yes | — | A Gradle task for packaging universal APK, eg. 'packageEnterpriseUniversalApk' | -| `ANDROID_UPLOAD_GRADLE_TASK` | `string` | Yes | — | A Gradle task for uploading APK, for example `appDistributionUploadEnterprise` | -| `KMP_FLAVOR` | `string` | Yes | — | KMP Build flavor. This is optional and only required by KMP projects and can be ignored on pure Android projects | -| `FIREBASE_APP_DISTRIBUTION_GROUPS` | `string` | Yes | — | Comma-separated list of app distribution group IDs | -| `TIMEOUT_MINUTES` | `number` | No | `30` | Job timeout in minutes | -| `USE_GIT_LFS` | `boolean` | No | `False` | Whether to download Git-LFS files | -| `GRADLE_OPTS` | `string` | No | — | Gradle options | -| `JAVA_VERSION` | `string` | No | `17` | Java version to use | -| `JAVA_DISTRIBUTION` | `string` | No | `zulu` | Java distribution to use | -| `ANDROID_VERSION_NAME` | `string` | No | — | Version name. Example: '1.X.X-snapshot' | -| `ANDROID_BUILD_NUMBER_OFFSET` | `number` | No | `0` | Build number offset. This number will be added to GITHUB_RUN_NUMBER and can be used to make corrections to build numbers. | -| `KMP_SWIFT_PACKAGE_INTEGRATION` | `boolean` | No | `False` | Whether KMP is integrated in Xcode project as a Swift Package | -| `KMP_SWIFT_PACKAGE_PATH` | `string` | No | — | If `KMP_SWIFT_PACKAGE_INTEGRATION` is 'true', then specifies a location of local Swift Package with Makefile. Example: 'iosApp/shared/KMP` | -| `ANDROID_SECRET_PROPERTIES_FILE` | `string` | No | `secrets.properties` | A path to file that will be populated with contents of 'android_secret_properties' secret. This file can be picked up by Secrets Gradle plugin to embed secrets into BuildConfig. | -| `IOS_SECRET_XCCONFIG_PATH` | `string` | No | — | Path to the .xcconfig file. Selected secret properties will be appended to the end of this file. | -| `IOS_SECRET_REQUIRED_KEYS` | `string` | No | — | Comma-separated list of required secret keys. | -| `IOS_CUSTOM_BUILD_PATH` | `string` | No | — | Path to a folder where the iOS code is located and where bundle exec fastlane is executed. This should be relative to iosApp folder | -| `IOS_CUSTOM_VALUES` | `string` | No | — | Custom string that can contains values specified in your workflow file. Those values will be placed into environment variable. Example: "CUSTOM-1: 1; CUSTOM-2: 2" | -| `CHANGELOG_DEBUG` | `boolean` | No | `False` | Enable debug mode for changelog generation. Default is false. | -| `CHANGELOG_CHECKOUT_DEPTH` | `number` | No | `100` | The depth of the git history to fetch for changelog generation. Default is 100. | -| `CHANGELOG_FALLBACK_LOOKBACK` | `string` | No | `24 hours` | The amount of time to look back for merge commits when no previous build commit is found. Default is 24 hours. | -| `JIRA_TRANSITION` | `string` | No | `Testing` | The name of the JIRA transition to apply to tickets found in merged branches. | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT` | Yes | JSON key of service account with permissions to upload build to Firebase App Distribution | -| `GRADLE_CACHE_ENCRYPTION_KEY` | No | Configuration cache encryption key | -| `ANDROID_SECRET_PROPERTIES` | No | Custom string that contains key-value properties as secrets. Contents of this secret will be placed into file specified by 'ANDROID_SECRET_PROPERTIES_FILE' input. | -| `IOS_SECRET_PROPERTIES` | No | Secrets in the format KEY = VALUE (one per line). | -| `IOS_MATCH_PASSWORD` | Yes | Password for decrypting of certificates and provisioning profiles. | -| `IOS_APP_STORE_CONNECT_API_KEY_KEY` | Yes | Private App Store Connect API key for submitting build to App Store. | -| `IOS_APP_STORE_CONNECT_API_KEY_KEY_ID` | Yes | Private App Store Connect API key for submitting build to App Store. | -| `IOS_APP_STORE_CONNECT_API_KEY_ISSUER_ID` | Yes | Private App Store Connect API issuer key for submitting build to App Store. | -| `JIRA_CONTEXT` | No | JIRA context for transitioning tickets. | - -## Internal Actions Used - -- [`Detect Changes & Changelog`](../../actions/utility/detect-changes-changelog.md) -- [`Setup Environment`](../../actions/android/setup-environment.md) -- [`KMP Build`](../../actions/ios/kmp-build.md) -- [`Build Firebase`](../../actions/android/build-firebase.md) -- [`JIRA Transition Tickets`](../../actions/utility/jira-transition-tickets.md) - diff --git a/.github/docs/workflows/kmp/index.md b/.github/docs/workflows/kmp/index.md deleted file mode 100644 index 1d0aea2..0000000 --- a/.github/docs/workflows/kmp/index.md +++ /dev/null @@ -1,11 +0,0 @@ - - -# KMP Workflows - -Reusable GitHub Actions workflows for KMP projects. - -| Name | Description | -|------|-------------| -| [KMP Detect Changes](cloud-detect-changes.md) | Detect Changes | -| [KMP Combined Nightly Build](combined-nightly-build.md) | KMP nightly build | - diff --git a/.github/docs/workflows/universal/cloud-backup.md b/.github/docs/workflows/universal/cloud-backup.md deleted file mode 100644 index 5156945..0000000 --- a/.github/docs/workflows/universal/cloud-backup.md +++ /dev/null @@ -1,38 +0,0 @@ - - -# Cloud Backup - -**Source:** [`workflows/universal-cloud-backup.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/universal-cloud-backup.yml) - -**Runner:** `ubuntu-latest` - -*Backup* - -## Usage - -```yaml -jobs: - cloud-backup: - uses: futuredapp/.github/.github/workflows/universal-cloud-backup.yml@main - with: - remote: '...' - secrets: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `host` | `string` | No | `github.com` | Host name. | -| `remote` | `string` | Yes | — | Remote SSH repository address. | -| `use_git_lfs` | `boolean` | No | — | Whether to download Git-LFS files. | -| `push_tags` | `boolean` | No | `False` | Whether to also push tags to backup origin. | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `SSH_PRIVATE_KEY` | Yes | Key for accessing repo with Apple certificates and provisioning profiles and repo with imported Fastlane lanes. - | - diff --git a/.github/docs/workflows/universal/index.md b/.github/docs/workflows/universal/index.md deleted file mode 100644 index e73d7b1..0000000 --- a/.github/docs/workflows/universal/index.md +++ /dev/null @@ -1,12 +0,0 @@ - - -# Universal Workflows - -Reusable GitHub Actions workflows for Universal projects. - -| Name | Description | -|------|-------------| -| [Cloud Backup](cloud-backup.md) | Backup | -| [Self-hosted Backup](selfhosted-backup.md) | Backup | -| [Workflows Lint](workflows-lint.md) | Check Pull Request | - diff --git a/.github/docs/workflows/universal/selfhosted-backup.md b/.github/docs/workflows/universal/selfhosted-backup.md deleted file mode 100644 index 9d83e2f..0000000 --- a/.github/docs/workflows/universal/selfhosted-backup.md +++ /dev/null @@ -1,38 +0,0 @@ - - -# Self-hosted Backup - -**Source:** [`workflows/universal-selfhosted-backup.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/universal-selfhosted-backup.yml) - -**Runner:** `Self-hosted` - -*Backup* - -## Usage - -```yaml -jobs: - self-hosted-backup: - uses: futuredapp/.github/.github/workflows/universal-selfhosted-backup.yml@main - with: - remote: '...' - secrets: - SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} -``` - -## Inputs - -| Name | Type | Required | Default | Description | -|------|------|----------|---------|-------------| -| `host` | `string` | No | `github.com` | Host name. | -| `remote` | `string` | Yes | — | Remote SSH repository address. | -| `use_git_lfs` | `boolean` | No | — | Whether to download Git-LFS files. | -| `push_tags` | `boolean` | No | `False` | Whether to also push tags to backup origin. | - -## Secrets - -| Name | Required | Description | -|------|----------|-------------| -| `SSH_PRIVATE_KEY` | Yes | Key for accessing repo with Apple certificates and provisioning profiles and repo with imported Fastlane lanes. - | - diff --git a/.github/docs/workflows/universal/workflows-lint.md b/.github/docs/workflows/universal/workflows-lint.md deleted file mode 100644 index 0ea57c0..0000000 --- a/.github/docs/workflows/universal/workflows-lint.md +++ /dev/null @@ -1,13 +0,0 @@ - - -# Workflows Lint - -!!! info "Internal Workflow" - This is not a reusable workflow — it runs directly on `pull_request` events in this repository. - -**Source:** [`workflows/workflows-lint.yml`](https://github.com/futuredapp/.github/blob/main/.github/workflows/workflows-lint.yml) - -**Runner:** `ubuntu-latest` - -*Check Pull Request* - From 86809b51a062bcc7c1bdb75ee070c59bfce4a3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20S=CC=8Cesta=CC=81k?= Date: Fri, 20 Feb 2026 12:07:00 +0000 Subject: [PATCH 05/10] chore: Remove auto-generated comment from templates Output files are now gitignored, so the warning is unnecessary. Co-Authored-By: Claude Opus 4.6 --- .github/scripts/templates/action.md.j2 | 2 -- .github/scripts/templates/index.md.j2 | 2 -- .github/scripts/templates/workflow.md.j2 | 2 -- 3 files changed, 6 deletions(-) diff --git a/.github/scripts/templates/action.md.j2 b/.github/scripts/templates/action.md.j2 index fd88c99..90bbc95 100644 --- a/.github/scripts/templates/action.md.j2 +++ b/.github/scripts/templates/action.md.j2 @@ -1,5 +1,3 @@ - - # {{ title }} **Source:** [`{{ source_path }}`](https://github.com/futuredapp/.github/blob/main/.github/{{ source_path }}) diff --git a/.github/scripts/templates/index.md.j2 b/.github/scripts/templates/index.md.j2 index 67e231a..a9a969b 100644 --- a/.github/scripts/templates/index.md.j2 +++ b/.github/scripts/templates/index.md.j2 @@ -1,5 +1,3 @@ - - # {{ title }} {{ description }} diff --git a/.github/scripts/templates/workflow.md.j2 b/.github/scripts/templates/workflow.md.j2 index e14f58f..d06e5ad 100644 --- a/.github/scripts/templates/workflow.md.j2 +++ b/.github/scripts/templates/workflow.md.j2 @@ -1,5 +1,3 @@ - - # {{ title }} {% if deprecated %} From e760fb0f3607a5b090c614929a4621b86e92df30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20S=CC=8Cesta=CC=81k?= Date: Fri, 20 Feb 2026 12:28:36 +0000 Subject: [PATCH 06/10] refactor(docs): Auto-discover workflows and actions from filesystem Replace hand-maintained WORKFLOWS and ACTIONS dicts with auto-discovery functions that scan workflows/*.yml and actions/*/action.yml. Only EXCLUDE, OVERRIDES, and CATEGORY_LABELS remain manually maintained. Co-Authored-By: Claude Opus 4.6 --- .github/scripts/config.py | 403 ++++++++++++++++++-------------------- 1 file changed, 189 insertions(+), 214 deletions(-) diff --git a/.github/scripts/config.py b/.github/scripts/config.py index f824af9..4a7ae0e 100644 --- a/.github/scripts/config.py +++ b/.github/scripts/config.py @@ -1,229 +1,204 @@ -"""Registry mapping every workflow and action to its documentation metadata.""" +"""Auto-discovered registry of workflows and actions for documentation.""" from __future__ import annotations -# Categories: ios, ios-kmp, android, kmp, universal, utility +from pathlib import Path -WORKFLOWS: dict[str, dict] = { - "ios-selfhosted-test": { - "source": "workflows/ios-selfhosted-test.yml", - "category": "ios", - "title": "iOS Test", - "output": "docs/workflows/ios/selfhosted-test.md", - "runner": "Self-hosted", - }, - "ios-selfhosted-nightly-build": { - "source": "workflows/ios-selfhosted-nightly-build.yml", - "category": "ios", - "title": "iOS Nightly Build", - "output": "docs/workflows/ios/selfhosted-nightly-build.md", - "runner": "Self-hosted", - }, - "ios-selfhosted-on-demand-build": { - "source": "workflows/ios-selfhosted-on-demand-build.yml", - "category": "ios", - "title": "iOS On-Demand Build", - "output": "docs/workflows/ios/selfhosted-on-demand-build.md", - "runner": "Self-hosted", - }, - "ios-selfhosted-release": { - "source": "workflows/ios-selfhosted-release.yml", - "category": "ios", - "title": "iOS Release", - "output": "docs/workflows/ios/selfhosted-release.md", - "runner": "Self-hosted", - }, +import yaml + +ROOT_DIR = Path(__file__).resolve().parent.parent + +# --------------------------------------------------------------------------- +# Manual configuration +# --------------------------------------------------------------------------- + +CATEGORY_LABELS: dict[str, str] = { + "ios": "iOS", + "ios-kmp": "iOS + KMP", + "android": "Android", + "kmp": "KMP", + "universal": "Universal", + "utility": "Utility", +} + +EXCLUDE: set[str] = {"deploy-docs"} + +# Per-entry overrides keyed by workflow/action name (filename stem or dir name). +# Any key set here replaces the auto-discovered value. +# +# Supported keys (workflows): +# source – relative path to the YAML file +# category – category id (must exist in CATEGORY_LABELS) +# title – display title (default: YAML `name:` field) +# output – output markdown path +# runner – runner label shown in docs +# not_reusable – bool; True hides the "Usage" snippet (auto-detected +# when `workflow_call` trigger is absent) +# deprecated – bool; True marks the workflow as deprecated +# deprecated_message – markdown string shown in the deprecation banner +# +# Supported keys (actions): +# source – relative path to action.yml +# category – category id (must exist in CATEGORY_LABELS) +# title – display title (default: YAML `name:` field) +# output – output markdown path +# readme – relative path to a README.md to embed (auto-detected) +# +OVERRIDES: dict[str, dict] = { "ios-selfhosted-build": { - "source": "workflows/ios-selfhosted-build.yml", - "category": "ios", "title": "iOS Build (Deprecated)", - "output": "docs/workflows/ios/selfhosted-build.md", - "runner": "Self-hosted", "deprecated": True, "deprecated_message": "Use `ios-selfhosted-nightly-build` instead.", }, - "ios-kmp-selfhosted-test": { - "source": "workflows/ios-kmp-selfhosted-test.yml", - "category": "ios-kmp", - "title": "iOS KMP Test", - "output": "docs/workflows/ios-kmp/selfhosted-test.md", - "runner": "Self-hosted", - }, - "ios-kmp-selfhosted-build": { - "source": "workflows/ios-kmp-selfhosted-build.yml", - "category": "ios-kmp", - "title": "iOS KMP Build", - "output": "docs/workflows/ios-kmp/selfhosted-build.md", - "runner": "Self-hosted", - }, - "ios-kmp-selfhosted-release": { - "source": "workflows/ios-kmp-selfhosted-release.yml", - "category": "ios-kmp", - "title": "iOS KMP Release", - "output": "docs/workflows/ios-kmp/selfhosted-release.md", - "runner": "Self-hosted", - }, - "android-cloud-check": { - "source": "workflows/android-cloud-check.yml", - "category": "android", - "title": "Android PR Check", - "output": "docs/workflows/android/cloud-check.md", - "runner": "ubuntu-latest", - }, - "android-cloud-nightly-build": { - "source": "workflows/android-cloud-nightly-build.yml", - "category": "android", - "title": "Android Nightly Build", - "output": "docs/workflows/android/cloud-nightly-build.md", - "runner": "ubuntu-latest", - }, - "android-cloud-release-firebase": { - "source": "workflows/android-cloud-release-firebaseAppDistribution.yml", - "category": "android", - "title": "Android Release (Firebase)", - "output": "docs/workflows/android/cloud-release-firebase.md", - "runner": "ubuntu-latest", - }, - "android-cloud-release-googleplay": { - "source": "workflows/android-cloud-release-googlePlay.yml", - "category": "android", - "title": "Android Release (Google Play)", - "output": "docs/workflows/android/cloud-release-googleplay.md", - "runner": "ubuntu-latest", - }, - "android-cloud-generate-baseline-profiles": { - "source": "workflows/android-cloud-generate-baseline-profiles.yml", - "category": "android", - "title": "Android Generate Baseline Profiles", - "output": "docs/workflows/android/cloud-generate-baseline-profiles.md", - "runner": "ubuntu-latest", - }, - "kmp-cloud-detect-changes": { - "source": "workflows/kmp-cloud-detect-changes.yml", - "category": "kmp", - "title": "KMP Detect Changes", - "output": "docs/workflows/kmp/cloud-detect-changes.md", - "runner": "ubuntu-latest", - }, - "kmp-combined-nightly-build": { - "source": "workflows/kmp-combined-nightly-build.yml", - "category": "kmp", - "title": "KMP Combined Nightly Build", - "output": "docs/workflows/kmp/combined-nightly-build.md", - "runner": "Self-hosted + ubuntu-latest", - }, - "universal-cloud-backup": { - "source": "workflows/universal-cloud-backup.yml", - "category": "universal", - "title": "Cloud Backup", - "output": "docs/workflows/universal/cloud-backup.md", - "runner": "ubuntu-latest", - }, - "universal-selfhosted-backup": { - "source": "workflows/universal-selfhosted-backup.yml", - "category": "universal", - "title": "Self-hosted Backup", - "output": "docs/workflows/universal/selfhosted-backup.md", - "runner": "Self-hosted", - }, "workflows-lint": { - "source": "workflows/workflows-lint.yml", - "category": "universal", - "title": "Workflows Lint", - "output": "docs/workflows/universal/workflows-lint.md", - "runner": "ubuntu-latest", "not_reusable": True, }, } -ACTIONS: dict[str, dict] = { - "android-setup-environment": { - "source": "actions/android-setup-environment/action.yml", - "category": "android", - "title": "Setup Environment", - "output": "docs/actions/android/setup-environment.md", - }, - "android-check": { - "source": "actions/android-check/action.yml", - "category": "android", - "title": "Android Check", - "output": "docs/actions/android/check.md", - }, - "android-build-firebase": { - "source": "actions/android-build-firebase/action.yml", - "category": "android", - "title": "Build Firebase", - "output": "docs/actions/android/build-firebase.md", - }, - "android-build-googleplay": { - "source": "actions/android-build-googlePlay/action.yml", - "category": "android", - "title": "Build Google Play", - "output": "docs/actions/android/build-googleplay.md", - }, - "android-generate-baseline-profiles": { - "source": "actions/android-generate-baseline-profiles/action.yml", - "category": "android", - "title": "Generate Baseline Profiles", - "output": "docs/actions/android/generate-baseline-profiles.md", - }, - "ios-export-secrets": { - "source": "actions/ios-export-secrets/action.yml", - "category": "ios", - "title": "Export Secrets", - "output": "docs/actions/ios/export-secrets.md", - }, - "ios-fastlane-test": { - "source": "actions/ios-fastlane-test/action.yml", - "category": "ios", - "title": "Fastlane Test", - "output": "docs/actions/ios/fastlane-test.md", - }, - "ios-fastlane-beta": { - "source": "actions/ios-fastlane-beta/action.yml", - "category": "ios", - "title": "Fastlane Beta", - "output": "docs/actions/ios/fastlane-beta.md", - }, - "ios-fastlane-release": { - "source": "actions/ios-fastlane-release/action.yml", - "category": "ios", - "title": "Fastlane Release", - "output": "docs/actions/ios/fastlane-release.md", - }, - "ios-kmp-build": { - "source": "actions/ios-kmp-build/action.yml", - "category": "ios", - "title": "KMP Build", - "output": "docs/actions/ios/kmp-build.md", - }, - "kmp-detect-changes": { - "source": "actions/kmp-detect-changes/action.yml", - "category": "utility", - "title": "KMP Detect Changes", - "output": "docs/actions/utility/kmp-detect-changes.md", - }, - "universal-detect-changes-and-generate-changelog": { - "source": "actions/universal-detect-changes-and-generate-changelog/action.yml", - "category": "utility", - "title": "Detect Changes & Changelog", - "output": "docs/actions/utility/detect-changes-changelog.md", - "readme": "actions/universal-detect-changes-and-generate-changelog/README.md", - }, - "jira-transition-tickets": { - "source": "actions/jira-transition-tickets/action.yml", - "category": "utility", - "title": "JIRA Transition Tickets", - "output": "docs/actions/utility/jira-transition-tickets.md", - "readme": "actions/jira-transition-tickets/README.md", - }, -} +# Ordered longest-first so "ios-kmp" matches before "ios". +CATEGORY_PREFIXES: list[str] = sorted( + ["ios-kmp", "ios", "android", "kmp", "universal"], + key=len, + reverse=True, +) -CATEGORY_LABELS: dict[str, str] = { - "ios": "iOS", - "ios-kmp": "iOS + KMP", - "android": "Android", - "kmp": "KMP", - "universal": "Universal", - "utility": "Utility", -} +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _match_category(key: str, fallback: str) -> tuple[str, str]: + """Return (category, slug) by matching the longest category prefix. + + The slug is the remainder of *key* after stripping the prefix and its + trailing hyphen. If no prefix matches, *fallback* is used as category + and the full key becomes the slug. + """ + for prefix in CATEGORY_PREFIXES: + if key.startswith(prefix + "-"): + slug = key[len(prefix) + 1 :] + return prefix, slug + if key == prefix: + return prefix, key + return fallback, key + + +def _derive_runner(key: str, yaml_text: str) -> str: + """Derive the runner label from filename convention, falling back to YAML.""" + if "-combined-" in key: + return "Self-hosted + ubuntu-latest" + if "-selfhosted-" in key: + return "Self-hosted" + if "-cloud-" in key: + return "ubuntu-latest" + + # Fallback: parse first runs-on value from YAML. + for line in yaml_text.splitlines(): + stripped = line.strip() + if stripped.startswith("runs-on:"): + value = stripped.split(":", 1)[1].strip() + # Normalise common variations. + if "self-hosted" in value.lower(): + return "Self-hosted" + return value + return "ubuntu-latest" + + +def _parse_yaml_name(path: Path) -> str: + """Return the top-level ``name`` field from a YAML file.""" + with open(path) as f: + data = yaml.safe_load(f) + if data and isinstance(data, dict): + return data.get("name", path.stem) + return path.stem + + +def _has_workflow_call(path: Path) -> bool: + """Return True if the workflow declares a ``workflow_call`` trigger.""" + with open(path) as f: + text = f.read() + return "workflow_call" in text + + +# --------------------------------------------------------------------------- +# Auto-discovery +# --------------------------------------------------------------------------- + + +def discover_workflows(root: Path) -> dict[str, dict]: + """Scan ``workflows/*.yml`` and build the config dict.""" + workflows: dict[str, dict] = {} + workflows_dir = root / "workflows" + if not workflows_dir.is_dir(): + return workflows + + for path in sorted(workflows_dir.glob("*.yml")): + key = path.stem + if key in EXCLUDE: + continue + + category, slug = _match_category(key, "universal") + title = _parse_yaml_name(path) + + with open(path) as f: + yaml_text = f.read() + runner = _derive_runner(key, yaml_text) + + entry: dict = { + "source": f"workflows/{path.name}", + "category": category, + "title": title, + "output": f"docs/workflows/{category}/{slug}.md", + "runner": runner, + } + + if not _has_workflow_call(path): + entry["not_reusable"] = True + + # Merge overrides on top. + if key in OVERRIDES: + entry.update(OVERRIDES[key]) + + workflows[key] = entry + + return workflows + + +def discover_actions(root: Path) -> dict[str, dict]: + """Scan ``actions/*/action.yml`` and build the config dict.""" + actions: dict[str, dict] = {} + actions_dir = root / "actions" + if not actions_dir.is_dir(): + return actions + + for path in sorted(actions_dir.glob("*/action.yml")): + key = path.parent.name + category, slug = _match_category(key, "utility") + title = _parse_yaml_name(path) + + entry: dict = { + "source": f"actions/{key}/action.yml", + "category": category, + "title": title, + "output": f"docs/actions/{category}/{slug}.md", + } + + readme = path.parent / "README.md" + if readme.exists(): + entry["readme"] = f"actions/{key}/README.md" + + # Merge overrides on top. + if key in OVERRIDES: + entry.update(OVERRIDES[key]) + + actions[key] = entry + + return actions + + +# --------------------------------------------------------------------------- +# Exported registries (same interface as before) +# --------------------------------------------------------------------------- + +WORKFLOWS: dict[str, dict] = discover_workflows(ROOT_DIR) +ACTIONS: dict[str, dict] = discover_actions(ROOT_DIR) From d354e5da94d393898f2c4b4f8ec4b5d047876193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20S=CC=8Cesta=CC=81k?= Date: Fri, 20 Feb 2026 12:28:47 +0000 Subject: [PATCH 07/10] fix(ci): Replace unused mkdocs cache with pip cache in deploy-docs The .cache directory is only used by mkdocs-material plugins (social cards, blog, offline) which are not enabled. Use setup-python built-in pip cache instead to speed up dependency installation. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/deploy-docs.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index da93b97..21c8a5d 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -31,11 +31,8 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.x' - - uses: actions/cache@v4 - with: - key: mkdocs-${{ hashFiles('requirements-docs.txt') }} - path: .cache - restore-keys: mkdocs- + cache: 'pip' + cache-dependency-path: requirements-docs.txt - run: pip install -r requirements-docs.txt - name: Determine version and ref id: version From 73826006d188162fdbe021168e4a7842b11c50c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20S=CC=8Cesta=CC=81k?= Date: Fri, 20 Feb 2026 12:30:15 +0000 Subject: [PATCH 08/10] chore(ci): Bump actions/checkout to v6 and actions/setup-python to v6 Co-Authored-By: Claude Opus 4.6 --- .github/workflows/deploy-docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 21c8a5d..83ac78e 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -24,11 +24,11 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 fetch-tags: true - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: '3.x' cache: 'pip' From 673cbb9d3119a91c68ae238584eed708f54170c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20S=CC=8Cesta=CC=81k?= Date: Fri, 20 Feb 2026 12:38:36 +0000 Subject: [PATCH 09/10] feat(docs): Auto-detect README for workflows Detect companion markdown files (workflows/{key}.md) alongside workflow YAMLs, matching the existing README detection for actions. Co-Authored-By: Claude Opus 4.6 --- .github/scripts/config.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/scripts/config.py b/.github/scripts/config.py index 4a7ae0e..fb0e2b6 100644 --- a/.github/scripts/config.py +++ b/.github/scripts/config.py @@ -36,6 +36,8 @@ # when `workflow_call` trigger is absent) # deprecated – bool; True marks the workflow as deprecated # deprecated_message – markdown string shown in the deprecation banner +# readme – relative path to a companion .md to embed +# (auto-detected from workflows/{key}.md) # # Supported keys (actions): # source – relative path to action.yml @@ -155,6 +157,10 @@ def discover_workflows(root: Path) -> dict[str, dict]: if not _has_workflow_call(path): entry["not_reusable"] = True + readme = path.with_suffix(".md") + if readme.exists(): + entry["readme"] = f"workflows/{readme.name}" + # Merge overrides on top. if key in OVERRIDES: entry.update(OVERRIDES[key]) From fa3701e8c2fa75d4210fe889a4757663ed2569fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=CC=8Cimon=20S=CC=8Cesta=CC=81k?= Date: Fri, 20 Feb 2026 14:03:53 +0000 Subject: [PATCH 10/10] feat(docs): Add mike for versioned documentation Replace mkdocs gh-deploy with mike deploy to support multiple doc versions. Tag pushes publish under the tag name with a "latest" alias, while main-branch pushes publish under "main". The Material theme version selector is enabled via extra.version.provider. Co-Authored-By: Claude Opus 4.6 --- .github/mkdocs.yml | 2 ++ .github/requirements-docs.txt | 1 + .github/workflows/deploy-docs.yml | 30 +++++++++++++++++++++--------- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/mkdocs.yml b/.github/mkdocs.yml index 1d3370a..5798728 100644 --- a/.github/mkdocs.yml +++ b/.github/mkdocs.yml @@ -107,6 +107,8 @@ nav: copyright: Made with ❤️‍🔥 at Futured extra: + version: + provider: mike social: - icon: material/web name: Web diff --git a/.github/requirements-docs.txt b/.github/requirements-docs.txt index a13d819..d9c7df7 100644 --- a/.github/requirements-docs.txt +++ b/.github/requirements-docs.txt @@ -1,5 +1,6 @@ mkdocs>=1.6,<2 mkdocs-material>=9.5 mkdocs-glightbox>=0.4 +mike>=2.1 Jinja2>=3.1 PyYAML>=6.0 diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml index 83ac78e..48b2019 100644 --- a/.github/workflows/deploy-docs.yml +++ b/.github/workflows/deploy-docs.yml @@ -11,6 +11,7 @@ on: - 'docs/**' - 'mkdocs.yml' - 'scripts/**' + - 'requirements-docs.txt' workflow_dispatch: permissions: @@ -34,21 +35,32 @@ jobs: cache: 'pip' cache-dependency-path: requirements-docs.txt - run: pip install -r requirements-docs.txt - - name: Determine version and ref + - name: Determine version and alias id: version run: | - VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "dev") - echo "version=$VERSION" >> "$GITHUB_OUTPUT" if [[ "$GITHUB_REF" == refs/tags/* ]]; then - echo "ref=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" + VERSION="${GITHUB_REF#refs/tags/}" + { + echo "version=$VERSION" + echo "alias=latest" + echo "ref=$VERSION" + } >> "$GITHUB_OUTPUT" else - echo "ref=main" >> "$GITHUB_OUTPUT" + { + echo "version=main" + echo "alias=" + echo "ref=main" + } >> "$GITHUB_OUTPUT" fi - - name: Inject version into site title - run: | - sed -i "s/^site_name:.*/site_name: Futured CI\/CD Workflows ${{ steps.version.outputs.version }}/" mkdocs.yml - run: python scripts/generate-docs.py --ref ${{ steps.version.outputs.ref }} - run: | git config user.name github-actions[bot] git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - run: mkdocs gh-deploy --force + - name: Deploy versioned docs + run: | + if [[ -n "${{ steps.version.outputs.alias }}" ]]; then + mike deploy --push --update-aliases ${{ steps.version.outputs.version }} ${{ steps.version.outputs.alias }} + mike set-default --push latest + else + mike deploy --push main + fi