From 2605e9e8418cf40b1b46ece6cbedb0b8ab1dba97 Mon Sep 17 00:00:00 2001 From: NiftyAndy Date: Tue, 28 Jul 2026 03:22:40 -0400 Subject: [PATCH] feat(init): configure reusable workflow repository --- .github/scripts/init-repo.sh | 6 ++ .github/scripts/sync-template.sh | 39 +++++++ .github/template.yml | 1 + .github/template.yml.example | 1 + .github/workflows/test.yml | 172 +------------------------------ README.md | 8 +- src/cli.mjs | 5 + 7 files changed, 63 insertions(+), 169 deletions(-) diff --git a/.github/scripts/init-repo.sh b/.github/scripts/init-repo.sh index cd69a23c..a5cf48c6 100755 --- a/.github/scripts/init-repo.sh +++ b/.github/scripts/init-repo.sh @@ -11,6 +11,7 @@ force=false languages="${REPO_FOUNDRY_LANGUAGES:-auto}" features="${REPO_FOUNDRY_FEATURES:-all}" package_manager="${REPO_FOUNDRY_PACKAGE_MANAGER:-auto}" +runtime_repository="${REPO_FOUNDRY_RUNTIME_REPOSITORY:-0xPlayerOne/code-foundry}" bootstrap=true release_type="${REPO_FOUNDRY_RELEASE_TYPE:-auto}" npm_publish="${REPO_FOUNDRY_NPM_PUBLISH:-false}" @@ -37,6 +38,7 @@ Options: --features LIST all or comma-separated optional features: ci,codeql,security,test,draft-pr,release-pr,release,dependabot --package-manager NAME auto, bun, pnpm, yarn, or npm + --runtime-repository OWNER/REPO Reusable workflow runtime repository --release-type NAME auto, node, python, rust, or simple --license NAME agpl-3.0-or-later, mit, preserve, or none --license-file PATH Use an exact custom license file @@ -63,6 +65,7 @@ while [ "$#" -gt 0 ]; do --languages) languages="${2:?missing language list}"; shift 2 ;; --features) features="${2:?missing feature list}"; shift 2 ;; --package-manager) package_manager="${2:?missing package manager}"; shift 2 ;; + --runtime-repository) runtime_repository="${2:?missing runtime repository}"; shift 2 ;; --release-type) release_type="${2:?missing release type}"; shift 2 ;; --license) license="${2:?missing license}"; shift 2 ;; --license-file) license_file="${2:?missing license file}"; shift 2 ;; @@ -108,6 +111,7 @@ sync_args=( --languages "$languages" --features "$features" --package-manager "$package_manager" + --runtime-repository "$runtime_repository" --license "$license" ) if [ -n "$license_file" ]; then sync_args+=(--license-file "$license_file"); fi @@ -127,6 +131,7 @@ profile="$(awk -F': ' '/^profile:/ {print $2; exit}' .github/template.yml 2>/dev languages="$(awk -F': ' '/^languages:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)" features="$(awk -F': ' '/^features:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)" package_manager="$(awk -F': ' '/^package_manager:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)" +runtime_repository="$(awk -F': ' '/^runtime_repository:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)" release_type="$(awk -F': ' '/^release_type:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)" npm_publish="$(awk -F': ' '/^npm_publish:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)" license="$(awk -F': ' '/^license:/ {print $2; exit}' .github/template.yml 2>/dev/null || true)" @@ -139,6 +144,7 @@ license="$(awk -F': ' '/^license:/ {print $2; exit}' .github/template.yml 2>/dev printf 'languages: %s\n' "$languages" printf 'features: %s\n' "$features" printf 'package_manager: %s\n' "$package_manager" + printf 'runtime_repository: %s\n' "$runtime_repository" printf 'release_type: %s\n' "$release_type" printf 'npm_publish: %s\n' "$npm_publish" printf 'license: %s\n' "$license" diff --git a/.github/scripts/sync-template.sh b/.github/scripts/sync-template.sh index 8658185f..2ed9831a 100755 --- a/.github/scripts/sync-template.sh +++ b/.github/scripts/sync-template.sh @@ -13,6 +13,7 @@ Options: --languages LIST auto or comma-separated: typescript,rust,python,solidity --features LIST all or comma-separated standard features --package-manager NAME auto, bun, pnpm, yarn, or npm + --runtime-repository OWNER/REPO Reusable workflow runtime repository --license NAME preserve, agpl-3.0-or-later, mit, or none --license-file PATH Use an exact custom license file --check Preview changes (default) @@ -40,6 +41,9 @@ features_set=false package_manager="${REPO_FOUNDRY_PACKAGE_MANAGER:-}" package_manager_set=false [ -n "${REPO_FOUNDRY_PACKAGE_MANAGER:-}" ] && package_manager_set=true +runtime_repository="${REPO_FOUNDRY_RUNTIME_REPOSITORY:-}" +runtime_repository_set=false +[ -n "${REPO_FOUNDRY_RUNTIME_REPOSITORY:-}" ] && runtime_repository_set=true template_ref="" release_type="${REPO_FOUNDRY_RELEASE_TYPE:-auto}" npm_publish="${REPO_FOUNDRY_NPM_PUBLISH:-false}" @@ -104,6 +108,7 @@ while [ "$#" -gt 0 ]; do --languages) languages="${2:?missing language list}"; languages_set=true; shift 2 ;; --features) features="${2:?missing feature list}"; features_set=true; shift 2 ;; --package-manager) package_manager="${2:?missing package manager}"; package_manager_set=true; shift 2 ;; + --runtime-repository) runtime_repository="${2:?missing runtime repository}"; runtime_repository_set=true; shift 2 ;; --license) license="${2:?missing license}"; license_set=true; shift 2 ;; --license-file) license_file="${2:?missing license file}"; shift 2 ;; --check) mode="check"; shift ;; @@ -132,6 +137,9 @@ if [ -f .github/template.yml ]; then if [ "$package_manager_set" = false ]; then package_manager="$(awk -F': ' '/^package_manager:/ {print $2; exit}' .github/template.yml)" fi + if [ "$runtime_repository_set" = false ]; then + runtime_repository="$(awk -F': ' '/^runtime_repository:/ {print $2; exit}' .github/template.yml)" + fi template_ref="$(awk -F': ' '/^template:/ {print $2; exit}' .github/template.yml)" if [ "$release_type_set" != true ]; then configured_release_type="$(awk -F': ' '/^release_type:/ {print $2; exit}' .github/template.yml)" @@ -147,6 +155,18 @@ if [ -f .github/template.yml ]; then fi fi [ -n "$package_manager" ] || package_manager=auto +if [ -z "$runtime_repository" ]; then + source_repository="$source" + if [ -d "$source" ]; then + source_repository="$(git -C "$source" remote get-url origin 2>/dev/null || true)" + fi + runtime_repository="$(printf '%s\n' "$source_repository" | sed -nE 's#.*github\.com[:/]([^/]+/[^/.]+)(\.git)?$#\1#p')" +fi +[ -n "$runtime_repository" ] || runtime_repository="0xPlayerOne/code-foundry" +case "$runtime_repository" in + */*) ;; + *) printf 'Runtime repository must be OWNER/REPO: %s\n' "$runtime_repository" >&2; exit 2 ;; +esac case "$package_manager" in auto|bun|pnpm|yarn|npm) ;; *) printf 'Unsupported package manager: %s\n' "$package_manager" >&2; exit 2 ;; @@ -185,6 +205,7 @@ if [ -f "$template_root/.github/scripts/profile.sh" ]; then REPO_FOUNDRY_LANGUAGES="$languages" \ REPO_FOUNDRY_FEATURES="$features" \ REPO_FOUNDRY_PACKAGE_MANAGER="$package_manager" \ + REPO_FOUNDRY_RUNTIME_REPOSITORY="$runtime_repository" \ REPO_FOUNDRY_RELEASE_TYPE="$release_type" \ REPO_FOUNDRY_NPM_PUBLISH="$npm_publish" \ bash "$template_root/.github/scripts/profile.sh" detect --root "$PWD" @@ -354,6 +375,23 @@ for file in "${files[@]}"; do fi done +# Reusable workflow callers must use a literal repository/ref. Render the +# selected runtime repository while leaving custom workflows untouched. +for file in .github/workflows/ci.yml .github/workflows/test.yml; do + [ -f "$file" ] || continue + if grep -q '0xPlayerOne/code-foundry' "$file"; then + changed=$((changed + 1)) + if [ "$mode" = "check" ]; then + printf 'Would render runtime repository in %s\n' "$file" + else + rendered_workflow="$(mktemp)" + sed "s#0xPlayerOne/code-foundry#$runtime_repository#g" "$file" > "$rendered_workflow" + mv "$rendered_workflow" "$file" + printf 'Rendered runtime repository in %s\n' "$file" + fi + fi +done + write_license() { local owner license_source [ "$license" != preserve ] || return 0 @@ -526,6 +564,7 @@ if [ "$mode" = "apply" ]; then if [ -n "$package_manager" ]; then printf 'package_manager: %s\n' "$package_manager" fi + printf 'runtime_repository: %s\n' "$runtime_repository" printf 'release_type: %s\n' "$release_type" printf 'npm_publish: %s\n' "$npm_publish" printf 'license: %s\n' "$license" diff --git a/.github/template.yml b/.github/template.yml index 9ababf7e..38c4e0d8 100644 --- a/.github/template.yml +++ b/.github/template.yml @@ -4,6 +4,7 @@ profile: auto languages: typescript features: all package_manager: bun +runtime_repository: 0xPlayerOne/code-foundry release_type: node npm_publish: true license: agpl-3.0-or-later diff --git a/.github/template.yml.example b/.github/template.yml.example index fb65ffdf..bdd3b9f6 100644 --- a/.github/template.yml.example +++ b/.github/template.yml.example @@ -5,6 +5,7 @@ profile: auto languages: auto features: all package_manager: auto +runtime_repository: 0xPlayerOne/code-foundry release_type: auto npm_publish: false license: preserve diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5294acca..97c092f8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,172 +10,8 @@ on: permissions: contents: read -env: - TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} - TURBO_TEAM: ${{ vars.TURBO_TEAM }} - REPO_FOUNDRY_PROFILE: ${{ vars.REPO_FOUNDRY_PROFILE }} - REPO_FOUNDRY_LANGUAGES: ${{ vars.REPO_FOUNDRY_LANGUAGES }} - REPO_FOUNDRY_FEATURES: ${{ vars.REPO_FOUNDRY_FEATURES }} - REPO_FOUNDRY_PACKAGE_MANAGER: ${{ vars.REPO_FOUNDRY_PACKAGE_MANAGER }} - REPO_FOUNDRY_CACHE_PACKAGES: ${{ vars.REPO_FOUNDRY_CACHE_PACKAGES || 'auto' }} - REPO_FOUNDRY_CACHE_BUILD: ${{ vars.REPO_FOUNDRY_CACHE_BUILD || 'auto' }} - REPO_FOUNDRY_BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before || '' }} - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }} - cancel-in-progress: true - jobs: - unit: - name: Unit - # Keep ordinary suites on the lean image. Set the repository variable - # REPO_FOUNDRY_UNIT_RUNNER=ubuntu-latest for native/system-dependent tests. - runs-on: ${{ vars.REPO_FOUNDRY_UNIT_RUNNER || 'ubuntu-slim' }} - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 2 - - name: Detect - id: applicability - run: bash .github/scripts/ci.sh task_profile unit >> "$GITHUB_OUTPUT" - - name: Setup - if: steps.applicability.outputs.applicable == 'true' - uses: ./.github/actions/setup - with: - install: true - install-javascript: ${{ steps.applicability.outputs.javascript == 'true' }} - install-python: ${{ steps.applicability.outputs.python == 'true' }} - install-rust: ${{ steps.applicability.outputs.rust == 'true' }} - cache-build: false - cache-environment: false - cache-packages: ${{ vars.REPO_FOUNDRY_CACHE_PACKAGES || 'auto' }} - task: unit - task-javascript: ${{ steps.applicability.outputs.javascript }} - task-python: ${{ steps.applicability.outputs.python }} - task-rust: ${{ steps.applicability.outputs.rust }} - - name: Unit - if: steps.applicability.outputs.applicable == 'true' - run: bash .github/scripts/ci.sh unit - - name: Upload coverage - if: >- - always() && - steps.applicability.outputs.applicable == 'true' && - hashFiles('coverage/**', 'htmlcov/**', '.coverage*', 'target/llvm-cov/**') != '' - uses: actions/upload-artifact@v7 - with: - name: coverage-unit-${{ github.run_id }} - path: | - coverage/** - htmlcov/** - .coverage* - target/llvm-cov/** - compression-level: 1 - if-no-files-found: ignore - retention-days: 14 - - integration: - name: Integration - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 2 - - name: Detect - id: applicability - run: bash .github/scripts/ci.sh task_profile integration >> "$GITHUB_OUTPUT" - - name: Setup - if: steps.applicability.outputs.applicable == 'true' - uses: ./.github/actions/setup - with: - install: true - install-javascript: ${{ steps.applicability.outputs.javascript == 'true' }} - install-python: ${{ steps.applicability.outputs.python == 'true' }} - install-rust: ${{ steps.applicability.outputs.rust == 'true' }} - cache-build: ${{ steps.applicability.outputs.rust == 'true' }} - cache-environment: false - cache-save: ${{ github.event_name == 'push' }} - task: integration - task-javascript: ${{ steps.applicability.outputs.javascript }} - task-python: ${{ steps.applicability.outputs.python }} - task-rust: ${{ steps.applicability.outputs.rust }} - - name: Integration - if: steps.applicability.outputs.applicable == 'true' - run: bash .github/scripts/ci.sh integration - - e2e: - name: E2E - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 2 - - name: Detect - id: applicability - run: bash .github/scripts/ci.sh task_profile e2e >> "$GITHUB_OUTPUT" - - name: Cache browser binaries - if: steps.applicability.outputs.applicable == 'true' && steps.applicability.outputs.browser == 'true' - uses: ./.github/actions/cache - with: - save: ${{ github.event_name == 'push' }} - path: | - ~/.cache/ms-playwright - ~/.cache/Cypress - ~/.cache/puppeteer - key: ${{ runner.os }}-e2e-browsers-${{ hashFiles('**/package.json', '**/bun.lock', '**/bun.lockb', '**/pnpm-lock.yaml', '**/yarn.lock', '**/package-lock.json', '**/playwright.config.*', '**/cypress.config.*') }} - restore-keys: | - ${{ runner.os }}-e2e-browsers- - - name: Setup - if: steps.applicability.outputs.applicable == 'true' - uses: ./.github/actions/setup - with: - install: true - install-javascript: ${{ steps.applicability.outputs.javascript == 'true' }} - install-python: ${{ steps.applicability.outputs.python == 'true' }} - install-rust: ${{ steps.applicability.outputs.rust == 'true' }} - cache-build: false - cache-environment: false - cache-save: ${{ github.event_name == 'push' }} - task: e2e - task-javascript: ${{ steps.applicability.outputs.javascript }} - task-python: ${{ steps.applicability.outputs.python }} - task-rust: ${{ steps.applicability.outputs.rust }} - - name: E2E - if: steps.applicability.outputs.applicable == 'true' - run: bash .github/scripts/ci.sh e2e - - smoke: - name: Smoke - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - name: Checkout - uses: actions/checkout@v7 - with: - fetch-depth: 2 - - name: Detect - id: applicability - run: bash .github/scripts/ci.sh task_profile smoke >> "$GITHUB_OUTPUT" - - name: Setup - if: steps.applicability.outputs.applicable == 'true' - uses: ./.github/actions/setup - with: - install: true - install-javascript: ${{ steps.applicability.outputs.javascript == 'true' }} - install-python: ${{ steps.applicability.outputs.python == 'true' }} - install-rust: ${{ steps.applicability.outputs.rust == 'true' }} - cache-build: false - cache-environment: false - cache-save: ${{ github.event_name == 'push' }} - task: smoke - task-javascript: ${{ steps.applicability.outputs.javascript }} - task-python: ${{ steps.applicability.outputs.python }} - task-rust: ${{ steps.applicability.outputs.rust }} - - name: Smoke - if: steps.applicability.outputs.applicable == 'true' - run: bash .github/scripts/ci.sh smoke + test: + name: Test + uses: 0xPlayerOne/code-foundry/.github/workflows/reusable-test.yml@v0.8.0 + secrets: inherit diff --git a/README.md b/README.md index d011a410..3967f86d 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,12 @@ npx code-foundry init --license agpl-3.0-or-later npx code-foundry init --license-file ./legal/LICENSE.txt ``` +Reusable workflow callers default to this package's public runtime. Use +`--runtime-repository OWNER/REPO` when initializing from an organization fork; +the selected repository is saved in `.github/template.yml` and rendered into +the small `ci.yml` and `test.yml` wrappers automatically. The equivalent +environment variable is `REPO_FOUNDRY_RUNTIME_REPOSITORY`. + Initialization defaults to AGPL for new repositories. Synchronization defaults to `--license preserve`, so an existing repository's license is never replaced unless you explicitly select a license or provide `--license-file`. @@ -84,7 +90,7 @@ bash .github/scripts/init-repo.sh \ Supported profiles are `application`, `monorepo`, and `minimal`; use `auto` to detect one. Supported languages are `typescript`, `rust`, `python`, and `solidity`. Supported feature flags are `ci`, `codeql`, `security`, `test`, `draft-pr`, `release-pr`, `release`, and `dependabot`. Use `--prune` only when you explicitly want disabled standard workflows removed; custom workflows are always preserved. The selected profile is stored in `.github/template.yml`, which makes later syncs repeatable and lets workflows consume repository-specific settings without duplicating the template scripts. -Profile precedence is consistent everywhere: explicit CLI flags, then `REPO_FOUNDRY_*` GitHub repository variables/environment values, then `.github/template.yml`, then automatic detection and standard defaults. Useful repository variables include `REPO_FOUNDRY_PROFILE`, `REPO_FOUNDRY_LANGUAGES`, `REPO_FOUNDRY_FEATURES`, `REPO_FOUNDRY_PACKAGE_MANAGER`, `REPO_FOUNDRY_RUNNER`, `REPO_FOUNDRY_UNIT_RUNNER`, `REPO_FOUNDRY_CACHE_PACKAGES`, `REPO_FOUNDRY_CACHE_BUILD`, `REPO_FOUNDRY_COVERAGE_MINIMUM`, and `REPO_FOUNDRY_TURBO_REMOTE`. Use CLI flags for one-off initialization; use repository variables for local overrides that should not be committed. +Profile precedence is consistent everywhere: explicit CLI flags, then `REPO_FOUNDRY_*` GitHub repository variables/environment values, then `.github/template.yml`, then automatic detection and standard defaults. Useful repository variables include `REPO_FOUNDRY_PROFILE`, `REPO_FOUNDRY_LANGUAGES`, `REPO_FOUNDRY_FEATURES`, `REPO_FOUNDRY_PACKAGE_MANAGER`, `REPO_FOUNDRY_RUNTIME_REPOSITORY`, `REPO_FOUNDRY_RUNNER`, `REPO_FOUNDRY_UNIT_RUNNER`, `REPO_FOUNDRY_CACHE_PACKAGES`, `REPO_FOUNDRY_CACHE_BUILD`, `REPO_FOUNDRY_COVERAGE_MINIMUM`, and `REPO_FOUNDRY_TURBO_REMOTE`. Use CLI flags for one-off initialization; use repository variables for local overrides that should not be committed. Inspect the resolved profile at any time with `bash .github/scripts/profile.sh detect`; use `profile.sh get KEY` when another script needs one setting. diff --git a/src/cli.mjs b/src/cli.mjs index 42132139..c6c0d86f 100644 --- a/src/cli.mjs +++ b/src/cli.mjs @@ -22,6 +22,7 @@ Init/sync options: --languages LIST auto or typescript,rust,python,solidity --features LIST all or ci,codeql,security,test,draft-pr,release-pr,release,dependabot --package-manager NAME auto, bun, pnpm, yarn, or npm + --runtime-repository OWNER/REPO Reusable workflow runtime repository --release-type NAME auto, node, python, rust, simple, or none --license NAME agpl-3.0-or-later, mit, preserve, or none --license-file PATH Use an exact custom license file @@ -49,6 +50,7 @@ function parseArgs(argv) { languages: process.env.REPO_FOUNDRY_LANGUAGES || 'auto', features: process.env.REPO_FOUNDRY_FEATURES || 'all', packageManager: process.env.REPO_FOUNDRY_PACKAGE_MANAGER || 'auto', + runtimeRepository: process.env.REPO_FOUNDRY_RUNTIME_REPOSITORY || '0xPlayerOne/code-foundry', releaseType: process.env.REPO_FOUNDRY_RELEASE_TYPE || 'auto', license: process.env.REPO_FOUNDRY_LICENSE || (command === 'init' ? 'agpl-3.0-or-later' : 'preserve'), licenseFile: process.env.REPO_FOUNDRY_LICENSE_FILE || '', @@ -69,6 +71,7 @@ function parseArgs(argv) { ['--languages', 'languages'], ['--features', 'features'], ['--package-manager', 'packageManager'], + ['--runtime-repository', 'runtimeRepository'], ['--release-type', 'releaseType'], ['--license', 'license'], ['--license-file', 'licenseFile'], @@ -120,6 +123,7 @@ function main() { if (options.licenseFile) common.push('--license-file', options.licenseFile) if (options.languagesSet) common.push('--languages', options.languages) if (options.featuresSet) common.push('--features', options.features) + common.push('--runtime-repository', options.runtimeRepository) if (options.prune) common.push('--prune') if (options.force) common.push('--force') if (options.dryRun) common.push('--check') @@ -133,6 +137,7 @@ function main() { '--languages', options.languages, '--features', options.features, '--package-manager', options.packageManager, + '--runtime-repository', options.runtimeRepository, '--release-type', options.releaseType, '--license', options.license, ]