From f3e919371f72c085748280fc4b56809535dc8d99 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:38:44 -0500 Subject: [PATCH 01/22] ci: add file sync policy and dry-run workflow --- .github/file-sync-preview.md | 42 ++++ .github/file-sync.json | 285 ++++++++++++++++++++++ .github/scripts/file-sync-preview.mjs | 45 ++++ .github/workflows/__file-sync-preview.yml | 115 +++++++++ .github/workflows/__global-replicator.yml | 20 -- organizations.json | 7 + repository-properties.json | 41 ++++ tests/file-sync-policy.test.mjs | 92 +++++++ tests/file-sync-preview.test.mjs | 73 ++++++ 9 files changed, 700 insertions(+), 20 deletions(-) create mode 100644 .github/file-sync-preview.md create mode 100644 .github/file-sync.json create mode 100644 .github/scripts/file-sync-preview.mjs create mode 100644 .github/workflows/__file-sync-preview.yml create mode 100644 organizations.json create mode 100644 repository-properties.json create mode 100644 tests/file-sync-policy.test.mjs create mode 100644 tests/file-sync-preview.test.mjs diff --git a/.github/file-sync-preview.md b/.github/file-sync-preview.md new file mode 100644 index 00000000..fec0cdaa --- /dev/null +++ b/.github/file-sync-preview.md @@ -0,0 +1,42 @@ +# File sync preview + +The **File sync preview** workflow plans shared-file changes without creating target Git objects, branches, or +pull requests. `dry_run` is fixed to `true`; there is no input that enables delivery. The current global replicator +stays active. Its obsolete Docker replication step has been removed because `_docker.yml` no longer exists. + +The preview runs for relevant changes on internal pull requests and master, daily at 11:17 UTC, and manually. +Fork pull requests do not run the credentialed preview. After this workflow is merged, use **Run workflow** to +optionally narrow a run to an organization or an exact `owner/repository`. Empty inputs preview all configured targets. + +## Policy and selection + +- [organizations.json](../organizations.json) is the shared array of five organization names. +- [file-sync.json](file-sync.json) preserves the remaining legacy file groups and 33 cleanup paths. Existing general + files, including Renovate, retain their `sync` behavior. `create_if_missing` remains available for reviewed exceptions. +- [repository-properties.json](../repository-properties.json) records the definitions already provisioned in each org. + `sync_mode: excluded` skips a repository. Multi-select `workflow_profiles` selects additional file groups. +- Public, unarchived repositories are eligible by default, including forks. The source repository is skipped. + Existing mirror and Upptime exclusions in LizardByte-infrastructure continue to apply through their properties. +- Deselected profiles propose removing their configured files. General files remain, and another selected rule + can retain a shared path. Missing sources fail validation; deletion requires a profile/policy change or cleanup rule. +- PR templates target only the existing `LizardByte/.github`, `LizardByte-infrastructure/.github`, and + `SunshineStream/.github` repositories. Add `Themerr/.github` and `KokoStreamTV/.github` after creating them. +- The `docker` and `cpm` profile choices currently have no file mappings: the old Docker wrapper is cleanup-only, + and CPM maintenance is a separate central workflow. + +## Reviewing a run + +The run summary lists each repository's status and number of proposed file changes. Download its +`file-sync-preview--` artifact and inspect `results.json` for each exact path and create/update/remove +operation. Partial results are saved even when another target fails; a failed preview still fails the job. +Private/archived repositories, the skipped source repository, and empty organizations produce no result rows. + +The workflow uses the existing `GH_BOT_TOKEN`. It must be able to read repository metadata/contents and organization +custom properties across the listed orgs. Previewing requires no target write permission. Missing access is reported +per organization or repository; changing GitHub properties, credentials, or secrets is outside this workflow. + +## Production cutover + +Review the dry-run results first, then rehearse delivery, repeated runs, profile deselection, exact targets, and +existing-file preservation in disposable repositories. A separate reviewed change will disable the old writer and +enable the new writer. This PR does not perform that switch. diff --git a/.github/file-sync.json b/.github/file-sync.json new file mode 100644 index 00000000..f836fe71 --- /dev/null +++ b/.github/file-sync.json @@ -0,0 +1,285 @@ +{ + "version": 1, + "id": "org-sync", + "include_archived": false, + "include_forks": true, + "include_private": false, + "include_source": false, + "rules": [ + { + "id": "obsolete-files", + "files": [ + { + "destination": ".codeql-prebuild-cpp-Linux.sh", + "mode": "remove" + }, + { + "destination": ".codeql-prebuild-cpp-Windows.sh", + "mode": "remove" + }, + { + "destination": ".codeql-prebuild-cpp-macOS.sh", + "mode": "remove" + }, + { + "destination": ".github/label-actions.yml", + "mode": "remove" + }, + { + "destination": ".github/pr_release_template.md", + "mode": "remove" + }, + { + "destination": ".github/workflows/_docker.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/auto-create-pr.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/automerge.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/autoupdate.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/autoupdate-labeler.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/ci-docker.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/ci-qodana.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/codeql.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/common-lint.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/cpp-lint.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/dispatcher.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/issues.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/issues-stale.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/pull-requests.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/python-flake8.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/release-notifier.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/top_issues.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/update-changelog.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/update-docs.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/update-flathub-repo.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/update-homebrew-release.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/update-pacman-repo.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/update-winget-release.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/yaml-lint.yml", + "mode": "remove" + }, + { + "destination": "codecov.yml", + "mode": "remove" + }, + { + "destination": "qodana-dotnet.yaml", + "mode": "remove" + }, + { + "destination": "qodana-js.yaml", + "mode": "remove" + }, + { + "destination": "qodana-python.yaml", + "mode": "remove" + } + ] + }, + { + "id": "general", + "files": [ + { + "source": ".github/dependabot.yml" + }, + { + "source": ".github/semantic.yml" + }, + { + "source": ".github/workflows/_codeql.yml" + }, + { + "source": ".github/workflows/_common-lint.yml" + }, + { + "source": ".github/workflows/_top-issues.yml" + }, + { + "source": "renovate.json" + } + ] + }, + { + "id": "docs", + "profile": "docs", + "files": [ + { + "source": ".github/workflows/_update-docs.yml" + } + ] + }, + { + "id": "flathub", + "profile": "flathub", + "files": [ + { + "source": ".github/workflows/_update-flathub-repo.yml" + } + ] + }, + { + "id": "homebrew", + "profile": "homebrew", + "files": [ + { + "source": ".github/workflows/_update-homebrew-repo.yml" + } + ] + }, + { + "id": "pacman", + "profile": "pacman", + "files": [ + { + "source": ".github/workflows/_update-pacman-repo.yml" + } + ] + }, + { + "id": "winget", + "profile": "winget", + "files": [ + { + "source": ".github/workflows/_update-winget-repo.yml" + } + ] + }, + { + "id": "node_ci", + "profile": "node_ci", + "files": [ + { + "source": ".github/workflows/_ci-node.yml" + } + ] + }, + { + "id": "npm", + "profile": "npm", + "files": [ + { + "source": ".github/workflows/_update-npm.yml" + } + ] + }, + { + "id": "python_style", + "profile": "python_style", + "files": [ + { + "source": ".flake8" + } + ] + }, + { + "id": "cpp_style", + "profile": "cpp_style", + "files": [ + { + "source": ".clang-format" + } + ] + }, + { + "id": "custom_issues", + "profile": "custom_issues", + "files": [ + { + "source": ".github/ISSUE_TEMPLATE/config.yml" + } + ] + }, + { + "id": "release_notifications", + "profile": "release_notifications", + "files": [ + { + "source": ".github/workflows/_release-notifier.yml" + }, + { + "source": ".github/workflows/_update-changelog.yml" + } + ] + }, + { + "id": "organization-pr-template", + "repositories": [ + "LizardByte/.github", + "LizardByte-infrastructure/.github", + "SunshineStream/.github" + ], + "files": [ + { + "source": ".github/pull_request_template.md" + } + ] + } + ] +} diff --git a/.github/scripts/file-sync-preview.mjs b/.github/scripts/file-sync-preview.mjs new file mode 100644 index 00000000..516beefe --- /dev/null +++ b/.github/scripts/file-sync-preview.mjs @@ -0,0 +1,45 @@ +import {mkdir, writeFile} from 'node:fs/promises'; +import path from 'node:path'; + +/** + * Save read-only action results and publish their counts to the run summary. + * Failed action steps can still provide partial results for the other organizations. + * + * @param {object} options Actions core/context, environment, and optional output directory. + * @returns {Promise} The report written to the artifact. + */ +export async function writePreviewReport({core, context, env, outputDirectory = 'build/file-sync-preview'}) { + const results = JSON.parse(env.PREVIEW_RESULTS || '[]'); + if (!Array.isArray(results)) { + throw new TypeError('Expected an array of file sync results'); + } + const report = { + dry_run: true, + source_commit: context.sha, + source_ref: context.ref, + organization: env.PREVIEW_ORGANIZATION || null, + repository: env.PREVIEW_REPOSITORY || null, + outcome: env.PREVIEW_OUTCOME, + changed_repositories: Number(env.PREVIEW_CHANGED || 0), + failed_repositories: Number(env.PREVIEW_FAILED || 0), + results, + }; + await mkdir(outputDirectory, {recursive: true}); + await writeFile(path.join(outputDirectory, 'results.json'), `${JSON.stringify(report, null, 2)}\n`, 'utf8'); + core.summary.addHeading('File sync preview'); + core.summary.addRaw('Dry run only. The current global replicator remains active.\n\n'); + core.summary.addRaw(`Preview step outcome: **${report.outcome}**. See the step logs for discovery failures.\n\n`); + core.summary.addRaw(`Repositories with proposed changes: **${report.changed_repositories}**; ` + + `failed targets: **${report.failed_repositories}**.\n\n`); + core.summary.addTable([ + [ + {data: 'Repository', header: true}, + {data: 'Status', header: true}, + {data: 'File changes', header: true}, + ], + ...results.map(result => [result.repository, result.status, String(result.changes?.length ?? 0)]), + ]); + core.summary.addRaw('\nDownload the file-sync-preview artifact for exact paths, operations, warnings, and errors.\n'); + await core.summary.write(); + return report; +} diff --git a/.github/workflows/__file-sync-preview.yml b/.github/workflows/__file-sync-preview.yml new file mode 100644 index 00000000..34779a78 --- /dev/null +++ b/.github/workflows/__file-sync-preview.yml @@ -0,0 +1,115 @@ +--- +name: File sync preview +permissions: {} + +on: + pull_request: + paths: + - '.github/file-sync.json' + - '.github/file-sync-preview.md' + - '.github/scripts/file-sync-preview.mjs' + - '.github/workflows/_*.yml' + - '.github/dependabot.yml' + - '.github/semantic.yml' + - '.github/ISSUE_TEMPLATE/config.yml' + - '.github/pull_request_template.md' + - '.clang-format' + - '.flake8' + - 'renovate.json' + - 'organizations.json' + - 'repository-properties.json' + - 'tests/file-sync-*.test.mjs' + push: + branches: + - master + paths: + - '.github/file-sync.json' + - '.github/file-sync-preview.md' + - '.github/scripts/file-sync-preview.mjs' + - '.github/workflows/_*.yml' + - '.github/dependabot.yml' + - '.github/semantic.yml' + - '.github/ISSUE_TEMPLATE/config.yml' + - '.github/pull_request_template.md' + - '.clang-format' + - '.flake8' + - 'renovate.json' + - 'organizations.json' + - 'repository-properties.json' + - 'tests/file-sync-*.test.mjs' + schedule: + - cron: '17 11 * * *' + workflow_dispatch: + inputs: + organization: + description: 'Optional organization from organizations.json; leave empty to preview all organizations.' + required: false + type: string + default: '' + repository: + description: 'Optional exact owner/repository; leave empty to preview all configured targets.' + required: false + type: string + default: '' + +concurrency: + group: file-sync-preview-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + preview: + name: Preview shared files + if: >- + github.repository == 'LizardByte/.github' && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + permissions: + contents: read + runs-on: ubuntu-latest + steps: + - name: Checkout source policy and files + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Preview file replication + id: preview + uses: LizardByte/actions/actions/org_sync@d5471304498e623f670db93062e3769a5bf14192 # org_sync PR #348 + with: + config: .github/file-sync.json + organizations_file: organizations.json + organization: ${{ inputs.organization }} + repository: ${{ inputs.repository }} + # This workflow cannot deliver target commits, branches, or pull requests. + dry_run: 'true' + token: ${{ secrets.GH_BOT_TOKEN }} + + - name: Save preview report + if: >- + always() && !cancelled() && + (steps.preview.outcome == 'success' || steps.preview.outcome == 'failure') + env: + PREVIEW_RESULTS: ${{ steps.preview.outputs.results }} + PREVIEW_OUTCOME: ${{ steps.preview.outcome }} + PREVIEW_CHANGED: ${{ steps.preview.outputs.changed_repositories }} + PREVIEW_FAILED: ${{ steps.preview.outputs.failed_repositories }} + PREVIEW_ORGANIZATION: ${{ inputs.organization }} + PREVIEW_REPOSITORY: ${{ inputs.repository }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const path = require('node:path'); + const { pathToFileURL } = require('node:url'); + const script = path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/file-sync-preview.mjs'); + const { writePreviewReport } = await import(pathToFileURL(script).href); + await writePreviewReport({ core, context, env: process.env }); + + - name: Upload preview report + if: >- + always() && !cancelled() && + (steps.preview.outcome == 'success' || steps.preview.outcome == 'failure') + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: file-sync-preview-${{ github.run_id }}-${{ github.run_attempt }} + path: build/file-sync-preview/results.json + if-no-files-found: error + retention-days: 14 diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index 37be73d0..5ae3b2d5 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -288,26 +288,6 @@ jobs: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Replicating files (docker) - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 - with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .github/workflows/_docker.yml - topics_to_include: 'docker' - exclude_forked: false - destination: '' - - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Replicating files (c, cpp) uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 with: diff --git a/organizations.json b/organizations.json new file mode 100644 index 00000000..50b53439 --- /dev/null +++ b/organizations.json @@ -0,0 +1,7 @@ +[ + "LizardByte", + "LizardByte-infrastructure", + "SunshineStream", + "Themerr", + "KokoStreamTV" +] diff --git a/repository-properties.json b/repository-properties.json new file mode 100644 index 00000000..8634dcb3 --- /dev/null +++ b/repository-properties.json @@ -0,0 +1,41 @@ +{ + "properties": [ + { + "property_name": "sync_mode", + "value_type": "single_select", + "required": true, + "default_value": "included", + "description": "Shared file synchronization: included by default; excluded opts the repository out.", + "allowed_values": [ + "included", + "excluded" + ], + "values_editable_by": "org_actors", + "require_explicit_values": false + }, + { + "property_name": "workflow_profiles", + "value_type": "multi_select", + "required": false, + "default_value": null, + "description": "Additional shared workflow and configuration groups. No selection means general replication only.", + "allowed_values": [ + "cpm", + "cpp_style", + "custom_issues", + "docker", + "docs", + "flathub", + "homebrew", + "node_ci", + "npm", + "pacman", + "python_style", + "release_notifications", + "winget" + ], + "values_editable_by": "org_actors", + "require_explicit_values": false + } + ] +} diff --git a/tests/file-sync-policy.test.mjs b/tests/file-sync-policy.test.mjs new file mode 100644 index 00000000..0cbd12eb --- /dev/null +++ b/tests/file-sync-policy.test.mjs @@ -0,0 +1,92 @@ +import assert from 'node:assert/strict'; +import {existsSync, readFileSync} from 'node:fs'; +import test from 'node:test'; + +const policy = JSON.parse(readFileSync('.github/file-sync.json', 'utf8')); +const organizations = JSON.parse(readFileSync('organizations.json', 'utf8')); +const schema = JSON.parse(readFileSync('repository-properties.json', 'utf8')); +const legacy = readFileSync('.github/workflows/__global-replicator.yml', 'utf8'); +const preview = readFileSync('.github/workflows/__file-sync-preview.yml', 'utf8'); +const compareText = (left, right) => left.localeCompare(right); +const profiles = new Map([ + ['', undefined], ['rtd', 'docs'], ['flathub-pkg', 'flathub'], ['homebrew-pkg', 'homebrew'], + ['pacman-pkg', 'pacman'], ['winget-pkg', 'winget'], ['nodejs', 'node_ci'], ['npm-pkg', 'npm'], + ['python', 'python_style'], ['c,cpp', 'cpp_style'], ['replicator-custom-issues', 'custom_issues'], + ['replicator-release-notifications', 'release_notifications'], +]); + +test('file policy preserves every active legacy file group and its profile selection', () => { + const groups = [...legacy.matchAll(/patterns_to_include: >-\r?\n([\s\S]*?)\r?\n\s+topics_to_include: '([^']*)'/g)]; + assert.equal(groups.length, 12); + for (const [, sourceList, topics] of groups) { + assert.ok(profiles.has(topics), `Unmapped legacy selector: ${topics}`); + const profile = profiles.get(topics); + const rules = policy.rules.filter(rule => rule.profile === profile && !rule.repositories); + for (const source of sourceList.trim().split(/,\s*/)) { + const mapping = rules.flatMap(rule => rule.files).find(file => file.source === source); + assert.ok(mapping, `Missing legacy source: ${source}`); + assert.equal(mapping.destination ?? mapping.source, source); + assert.equal(mapping.mode ?? 'sync', 'sync'); + } + } +}); + +test('cleanup remains explicit and the obsolete Docker wrapper is never a source', () => { + const cleanup = legacy.match(/patterns_to_remove: >-\r?\n([\s\S]*?)\r?\n\s+topics_to_include:/)[1]; + const expected = cleanup.trim().split(/,\s*/); + const actual = policy.rules.flatMap(rule => rule.files).filter(file => file.mode === 'remove'); + assert.deepEqual(actual.map(file => file.destination).toSorted(compareText), expected.toSorted(compareText)); + assert.ok(actual.some(file => file.destination === '.github/workflows/_docker.yml')); + assert.ok(actual.every(file => file.source === undefined)); + assert.ok(policy.rules.flatMap(rule => rule.files).every(file => file.source !== '.github/workflows/_docker.yml')); + assert.doesNotMatch(legacy, /name: Replicating files \(docker\)/); +}); + +test('configured sources exist and policy selectors use the shared organization list and property schema', () => { + assert.equal(policy.version, 1); + assert.ok(organizations.every(org => typeof org === 'string')); + assert.equal(new Set(organizations).size, organizations.length); + const profileSchema = schema.properties.find(property => property.property_name === 'workflow_profiles'); + for (const rule of policy.rules) { + if (rule.profile) { + assert.ok(profileSchema.allowed_values.includes(rule.profile), `Unknown profile: ${rule.profile}`); + } + for (const org of rule.organizations ?? []) assert.ok(organizations.includes(org)); + for (const repo of rule.repositories ?? []) assert.ok(organizations.includes(repo.split('/')[0])); + for (const file of rule.files) { + if (file.source) assert.ok(existsSync(file.source), `Missing source file: ${file.source}`); + } + } + const sync = schema.properties.find(property => property.property_name === 'sync_mode'); + assert.equal(sync.required, true); + assert.equal(sync.default_value, 'included'); + assert.equal(profileSchema.value_type, 'multi_select'); + assert.equal(profileSchema.required, false); + assert.equal(profileSchema.default_value, null); + assert.deepEqual(profileSchema.allowed_values, profileSchema.allowed_values.toSorted(compareText)); +}); + +test('PR templates target only exact organization .github repositories', () => { + const templates = policy.rules.filter(rule => rule.files.some(file => file.source === '.github/pull_request_template.md')); + assert.equal(templates.length, 1); + assert.ok(templates[0].repositories.length > 0); + assert.ok(templates[0].repositories.every(repo => repo.endsWith('/.github'))); + assert.equal(templates[0].profile, undefined); + assert.equal(policy.include_source, false); + assert.equal(policy.include_private, false); + assert.equal(policy.include_archived, false); + assert.equal(policy.include_forks, true); +}); + +test('preview cannot enable delivery and the legacy writer stays enabled', () => { + const dispatch = preview.slice(preview.indexOf(' workflow_dispatch:'), preview.indexOf('\nconcurrency:')); + assert.doesNotMatch(dispatch, /dry_run|type: boolean/); + assert.match(preview, /dry_run: 'true'/); + assert.doesNotMatch(preview, /dry_run:.*\$\{\{/); + assert.match(preview, /LizardByte\/actions\/actions\/org_sync@d5471304498e623f670db93062e3769a5bf14192/); + assert.match(preview, /github\.event\.pull_request\.head\.repo\.full_name == github\.repository/); + assert.doesNotMatch(preview, /pull_request_target:|contents: write|pull-requests: write|continue-on-error:/); + assert.match(legacy, / push:\r?\n branches:\r?\n - master/); + assert.match(legacy, / workflow_dispatch:/); + assert.match(legacy, /uses: derberg\/manage-files-in-multiple-repositories@/); +}); diff --git a/tests/file-sync-preview.test.mjs b/tests/file-sync-preview.test.mjs new file mode 100644 index 00000000..27590bed --- /dev/null +++ b/tests/file-sync-preview.test.mjs @@ -0,0 +1,73 @@ +import assert from 'node:assert/strict'; +import {mkdtemp, readFile, rm} from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import {writePreviewReport} from '../.github/scripts/file-sync-preview.mjs'; + +function summary() { + const calls = []; + return { + calls, + addHeading: value => calls.push(['heading', value]), + addRaw: value => calls.push(['raw', value]), + addTable: value => calls.push(['table', value]), + write: async () => calls.push(['write']), + }; +} + +test('partial discovery failures retain exact changes and errors in the read-only artifact', async t => { + const directory = await mkdtemp(path.join(os.tmpdir(), 'file-sync-report-')); + t.after(() => rm(directory, {recursive: true, force: true})); + const output = summary(); + const results = [ + {repository: 'example/app', status: 'planned', changes: [{path: 'old.yml', operation: 'remove'}], warnings: []}, + {repository: 'other/*', status: 'failed', error: 'Property access denied'}, + {repository: 'example/mirror', status: 'excluded'}, + ]; + const report = await writePreviewReport({ + core: {summary: output}, context: {sha: 'revision', ref: 'refs/pull/1/merge'}, outputDirectory: directory, + env: {PREVIEW_RESULTS: JSON.stringify(results), PREVIEW_OUTCOME: 'failure', PREVIEW_CHANGED: '1', PREVIEW_FAILED: '1', + PREVIEW_ORGANIZATION: 'example', PREVIEW_REPOSITORY: 'example/app'}, + }); + assert.equal(report.dry_run, true); + assert.equal(report.outcome, 'failure'); + assert.equal(report.source_commit, 'revision'); + assert.equal(report.source_ref, 'refs/pull/1/merge'); + assert.equal(report.organization, 'example'); + assert.equal(report.repository, 'example/app'); + assert.equal(report.changed_repositories, 1); + assert.equal(report.failed_repositories, 1); + assert.deepEqual(report.results, results); + assert.deepEqual(JSON.parse(await readFile(path.join(directory, 'results.json'), 'utf8')), report); + assert.deepEqual(output.calls.find(([type]) => type === 'table')[1].slice(1), [ + ['example/app', 'planned', '1'], ['other/*', 'failed', '0'], ['example/mirror', 'excluded', '0'], + ]); + assert.ok(output.calls.some(([type, value]) => type === 'raw' && value.includes('global replicator remains active'))); + assert.deepEqual(output.calls.at(-1), ['write']); +}); + +test('an action failure without outputs still produces a diagnostic report', async t => { + const directory = await mkdtemp(path.join(os.tmpdir(), 'file-sync-report-')); + t.after(() => rm(directory, {recursive: true, force: true})); + const output = summary(); + const report = await writePreviewReport({ + core: {summary: output}, context: {sha: 'revision', ref: 'refs/heads/master'}, + env: {PREVIEW_OUTCOME: 'failure'}, outputDirectory: directory, + }); + assert.deepEqual(report.results, []); + assert.equal(report.changed_repositories, 0); + assert.equal(report.failed_repositories, 0); + assert.equal(report.organization, null); + assert.equal(report.repository, null); + assert.ok(output.calls.some(([type, value]) => type === 'raw' && value.includes('**failure**'))); + assert.equal(JSON.parse(await readFile(path.join(directory, 'results.json'), 'utf8')).dry_run, true); +}); + +test('malformed action output is rejected before writing a misleading summary', async () => { + const output = summary(); + await assert.rejects(writePreviewReport({core: {summary: output}, context: {}, env: {PREVIEW_RESULTS: '{}'}}), + /Expected an array of file sync results/); + assert.deepEqual(output.calls, []); +}); From e9b8e1183d1d916e970c1d40b2a4c4b24329e537 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:40:42 -0500 Subject: [PATCH 02/22] ci: replace global replicator with org sync --- .github/file-sync-preview.md | 42 -- .github/file-sync.md | 52 +++ .github/scripts/file-sync-preview.mjs | 45 -- .github/scripts/file-sync-report.mjs | 50 +++ .github/workflows/README.md | 6 +- .github/workflows/__file-sync-preview.yml | 115 ------ .github/workflows/__global-replicator.yml | 388 +++--------------- tests/file-sync-policy.test.mjs | 76 ++-- ...iew.test.mjs => file-sync-report.test.mjs} | 50 ++- 9 files changed, 249 insertions(+), 575 deletions(-) delete mode 100644 .github/file-sync-preview.md create mode 100644 .github/file-sync.md delete mode 100644 .github/scripts/file-sync-preview.mjs create mode 100644 .github/scripts/file-sync-report.mjs delete mode 100644 .github/workflows/__file-sync-preview.yml rename tests/{file-sync-preview.test.mjs => file-sync-report.test.mjs} (53%) diff --git a/.github/file-sync-preview.md b/.github/file-sync-preview.md deleted file mode 100644 index fec0cdaa..00000000 --- a/.github/file-sync-preview.md +++ /dev/null @@ -1,42 +0,0 @@ -# File sync preview - -The **File sync preview** workflow plans shared-file changes without creating target Git objects, branches, or -pull requests. `dry_run` is fixed to `true`; there is no input that enables delivery. The current global replicator -stays active. Its obsolete Docker replication step has been removed because `_docker.yml` no longer exists. - -The preview runs for relevant changes on internal pull requests and master, daily at 11:17 UTC, and manually. -Fork pull requests do not run the credentialed preview. After this workflow is merged, use **Run workflow** to -optionally narrow a run to an organization or an exact `owner/repository`. Empty inputs preview all configured targets. - -## Policy and selection - -- [organizations.json](../organizations.json) is the shared array of five organization names. -- [file-sync.json](file-sync.json) preserves the remaining legacy file groups and 33 cleanup paths. Existing general - files, including Renovate, retain their `sync` behavior. `create_if_missing` remains available for reviewed exceptions. -- [repository-properties.json](../repository-properties.json) records the definitions already provisioned in each org. - `sync_mode: excluded` skips a repository. Multi-select `workflow_profiles` selects additional file groups. -- Public, unarchived repositories are eligible by default, including forks. The source repository is skipped. - Existing mirror and Upptime exclusions in LizardByte-infrastructure continue to apply through their properties. -- Deselected profiles propose removing their configured files. General files remain, and another selected rule - can retain a shared path. Missing sources fail validation; deletion requires a profile/policy change or cleanup rule. -- PR templates target only the existing `LizardByte/.github`, `LizardByte-infrastructure/.github`, and - `SunshineStream/.github` repositories. Add `Themerr/.github` and `KokoStreamTV/.github` after creating them. -- The `docker` and `cpm` profile choices currently have no file mappings: the old Docker wrapper is cleanup-only, - and CPM maintenance is a separate central workflow. - -## Reviewing a run - -The run summary lists each repository's status and number of proposed file changes. Download its -`file-sync-preview--` artifact and inspect `results.json` for each exact path and create/update/remove -operation. Partial results are saved even when another target fails; a failed preview still fails the job. -Private/archived repositories, the skipped source repository, and empty organizations produce no result rows. - -The workflow uses the existing `GH_BOT_TOKEN`. It must be able to read repository metadata/contents and organization -custom properties across the listed orgs. Previewing requires no target write permission. Missing access is reported -per organization or repository; changing GitHub properties, credentials, or secrets is outside this workflow. - -## Production cutover - -Review the dry-run results first, then rehearse delivery, repeated runs, profile deselection, exact targets, and -existing-file preservation in disposable repositories. A separate reviewed change will disable the old writer and -enable the new writer. This PR does not perform that switch. diff --git a/.github/file-sync.md b/.github/file-sync.md new file mode 100644 index 00000000..a64a7554 --- /dev/null +++ b/.github/file-sync.md @@ -0,0 +1,52 @@ +# File synchronization + +The **Global replicator** workflow uses `LizardByte/actions/actions/org_sync` to reconcile shared files across +the organizations in [organizations.json](../organizations.json). It replaces the old topic-based replicator +and the separate preview workflow. A single checkout supplies the policy and source files. + +| Event | Mode | Scope | +| ------------------------ | -------- | ------------------------------------------ | +| Internal pull request | Dry run | All configured targets | +| Push to master | Delivery | All configured targets | +| Daily at 11:17 UTC | Delivery | All configured targets | +| Manual workflow dispatch | Delivery | All targets or the selected org/repository | + +Every pull request and master push runs the workflow regardless of which files changed. Fork pull requests +skip the credentialed job. PR runs never create target Git objects, branches, or pull requests. Delivery runs +create or update one sync PR per repository; they do not merge those PRs. Delivery runs share a concurrency +group and are not canceled by subsequent runs. A newer PR preview can cancel an older preview of the same PR. + +After merging this workflow, use **Run workflow** to optionally select an `organization` or exact +`owner/repository`. Empty inputs reconcile all configured targets. The former `repo_name` input is replaced +by the qualified `repository` input. The dedicated bot branch is `bot/org-sync-files`; the action does not +take over branches or PRs created by the old replicator. + +## Policy and selection + +- [organizations.json](../organizations.json) is the shared array of five organization names. +- [file-sync.json](file-sync.json) covers the existing file groups and 33 cleanup paths. General files, including + Renovate, use `sync`. `create_if_missing` is available for reviewed exceptions. +- [repository-properties.json](../repository-properties.json) records the definitions provisioned in each org. + `sync_mode: excluded` skips a repository. Multi-select `workflow_profiles` selects additional file groups. +- Public, unarchived repositories are eligible by default, including forks. The source repository is skipped. + Existing mirror and Upptime exclusions in LizardByte-infrastructure apply through their properties. +- Deselected profiles remove their configured files on the next run. General files remain, and another selected + rule can retain a shared path. Missing sources fail validation; deletion requires a profile/policy change or + explicit cleanup rule. `_docker.yml` is cleanup-only because its source no longer exists. +- PR templates target only the existing `LizardByte/.github`, `LizardByte-infrastructure/.github`, and + `SunshineStream/.github` repositories. Add `Themerr/.github` and `KokoStreamTV/.github` after creating them. +- The `docker` and `cpm` profile choices currently have no file mappings. CPM maintenance is a separate workflow. + +## Reviewing a run + +The run summary states whether delivery or dry-run mode was used and lists each repository's status and file +change count. Download `file-sync--` and inspect `results.json` for exact paths, operations, +warnings, errors, and target PR URLs. Partial results are saved even when another target fails; a failed sync +still fails the job. Private/archived repositories, the skipped source repository, and empty organizations +produce no result rows. + +The existing `GH_BOT_TOKEN` must read repository metadata/contents and organization custom properties across +the listed orgs. Delivery additionally requires permission to write contents/workflows and manage target PRs. +The token's user must have access in each organization. Missing access is reported per organization or repository. +The workflow's own `GITHUB_TOKEN` has only `contents: read`. Credentials, properties, secrets, and settings are +provisioned separately from file synchronization. diff --git a/.github/scripts/file-sync-preview.mjs b/.github/scripts/file-sync-preview.mjs deleted file mode 100644 index 516beefe..00000000 --- a/.github/scripts/file-sync-preview.mjs +++ /dev/null @@ -1,45 +0,0 @@ -import {mkdir, writeFile} from 'node:fs/promises'; -import path from 'node:path'; - -/** - * Save read-only action results and publish their counts to the run summary. - * Failed action steps can still provide partial results for the other organizations. - * - * @param {object} options Actions core/context, environment, and optional output directory. - * @returns {Promise} The report written to the artifact. - */ -export async function writePreviewReport({core, context, env, outputDirectory = 'build/file-sync-preview'}) { - const results = JSON.parse(env.PREVIEW_RESULTS || '[]'); - if (!Array.isArray(results)) { - throw new TypeError('Expected an array of file sync results'); - } - const report = { - dry_run: true, - source_commit: context.sha, - source_ref: context.ref, - organization: env.PREVIEW_ORGANIZATION || null, - repository: env.PREVIEW_REPOSITORY || null, - outcome: env.PREVIEW_OUTCOME, - changed_repositories: Number(env.PREVIEW_CHANGED || 0), - failed_repositories: Number(env.PREVIEW_FAILED || 0), - results, - }; - await mkdir(outputDirectory, {recursive: true}); - await writeFile(path.join(outputDirectory, 'results.json'), `${JSON.stringify(report, null, 2)}\n`, 'utf8'); - core.summary.addHeading('File sync preview'); - core.summary.addRaw('Dry run only. The current global replicator remains active.\n\n'); - core.summary.addRaw(`Preview step outcome: **${report.outcome}**. See the step logs for discovery failures.\n\n`); - core.summary.addRaw(`Repositories with proposed changes: **${report.changed_repositories}**; ` - + `failed targets: **${report.failed_repositories}**.\n\n`); - core.summary.addTable([ - [ - {data: 'Repository', header: true}, - {data: 'Status', header: true}, - {data: 'File changes', header: true}, - ], - ...results.map(result => [result.repository, result.status, String(result.changes?.length ?? 0)]), - ]); - core.summary.addRaw('\nDownload the file-sync-preview artifact for exact paths, operations, warnings, and errors.\n'); - await core.summary.write(); - return report; -} diff --git a/.github/scripts/file-sync-report.mjs b/.github/scripts/file-sync-report.mjs new file mode 100644 index 00000000..126d063f --- /dev/null +++ b/.github/scripts/file-sync-report.mjs @@ -0,0 +1,50 @@ +import {mkdir, writeFile} from 'node:fs/promises'; +import path from 'node:path'; + +/** + * Save preview or delivery results and publish their counts to the run summary. + * Failed action steps can still provide partial results for the other organizations. + * + * @param {object} options Actions core/context, environment, and optional output directory. + * @returns {Promise} The report written to the artifact. + */ +export async function writeSyncReport({core, context, env, outputDirectory = 'build/file-sync'}) { + if (!['true', 'false'].includes(env.SYNC_DRY_RUN)) { + throw new TypeError('Expected an explicit file sync dry-run mode'); + } + const results = JSON.parse(env.SYNC_RESULTS || '[]'); + if (!Array.isArray(results)) { + throw new TypeError('Expected an array of file sync results'); + } + const report = { + dry_run: env.SYNC_DRY_RUN === 'true', + source_commit: context.sha, + source_ref: context.ref, + organization: env.SYNC_ORGANIZATION || null, + repository: env.SYNC_REPOSITORY || null, + outcome: env.SYNC_OUTCOME, + changed_repositories: Number(env.SYNC_CHANGED || 0), + failed_repositories: Number(env.SYNC_FAILED || 0), + results, + }; + await mkdir(outputDirectory, {recursive: true}); + await writeFile(path.join(outputDirectory, 'results.json'), `${JSON.stringify(report, null, 2)}\n`, 'utf8'); + core.summary.addHeading(report.dry_run ? 'File sync preview' : 'File synchronization'); + core.summary.addRaw(report.dry_run + ? 'Dry run only. No target Git objects, branches, or pull requests were written.\n\n' + : 'Delivery enabled. Shared-file changes are proposed through target pull requests.\n\n'); + core.summary.addRaw(`Sync step outcome: **${report.outcome}**. See the step logs for failures.\n\n`); + core.summary.addRaw(`Repositories changed or planned: **${report.changed_repositories}**; ` + + `failed targets: **${report.failed_repositories}**.\n\n`); + core.summary.addTable([ + [ + {data: 'Repository', header: true}, + {data: 'Status', header: true}, + {data: 'File changes', header: true}, + ], + ...results.map(result => [result.repository, result.status, String(result.changes?.length ?? 0)]), + ]); + core.summary.addRaw('\nDownload the file-sync artifact for exact paths, operations, warnings, errors, and PR URLs.\n'); + await core.summary.write(); + return report; +} diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 69a9f990..5d933c87 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -10,8 +10,10 @@ respective repositories. Workflows that prefixed with a single `_` are replicated to other repositories and this indicates that the workflow should not be modified when inside the other repositories. -Repositories excluded from both the global replicator and GitLab mirror are listed in -`.github/repositories-to-ignore.json`. +The [global replicator](../file-sync.md) selects repositories through the `sync_mode` and `workflow_profiles` +custom properties. It previews every internal PR and delivers on master pushes, daily, or manually. + +Repositories excluded from the GitLab mirror are listed in `.github/repositories-to-ignore.json`. The GitLab mirror also excludes GitHub temporary security advisory forks matching `-ghsa-xxxx-xxxx-xxxx`. diff --git a/.github/workflows/__file-sync-preview.yml b/.github/workflows/__file-sync-preview.yml deleted file mode 100644 index 34779a78..00000000 --- a/.github/workflows/__file-sync-preview.yml +++ /dev/null @@ -1,115 +0,0 @@ ---- -name: File sync preview -permissions: {} - -on: - pull_request: - paths: - - '.github/file-sync.json' - - '.github/file-sync-preview.md' - - '.github/scripts/file-sync-preview.mjs' - - '.github/workflows/_*.yml' - - '.github/dependabot.yml' - - '.github/semantic.yml' - - '.github/ISSUE_TEMPLATE/config.yml' - - '.github/pull_request_template.md' - - '.clang-format' - - '.flake8' - - 'renovate.json' - - 'organizations.json' - - 'repository-properties.json' - - 'tests/file-sync-*.test.mjs' - push: - branches: - - master - paths: - - '.github/file-sync.json' - - '.github/file-sync-preview.md' - - '.github/scripts/file-sync-preview.mjs' - - '.github/workflows/_*.yml' - - '.github/dependabot.yml' - - '.github/semantic.yml' - - '.github/ISSUE_TEMPLATE/config.yml' - - '.github/pull_request_template.md' - - '.clang-format' - - '.flake8' - - 'renovate.json' - - 'organizations.json' - - 'repository-properties.json' - - 'tests/file-sync-*.test.mjs' - schedule: - - cron: '17 11 * * *' - workflow_dispatch: - inputs: - organization: - description: 'Optional organization from organizations.json; leave empty to preview all organizations.' - required: false - type: string - default: '' - repository: - description: 'Optional exact owner/repository; leave empty to preview all configured targets.' - required: false - type: string - default: '' - -concurrency: - group: file-sync-preview-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true - -jobs: - preview: - name: Preview shared files - if: >- - github.repository == 'LizardByte/.github' && - (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) - permissions: - contents: read - runs-on: ubuntu-latest - steps: - - name: Checkout source policy and files - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Preview file replication - id: preview - uses: LizardByte/actions/actions/org_sync@d5471304498e623f670db93062e3769a5bf14192 # org_sync PR #348 - with: - config: .github/file-sync.json - organizations_file: organizations.json - organization: ${{ inputs.organization }} - repository: ${{ inputs.repository }} - # This workflow cannot deliver target commits, branches, or pull requests. - dry_run: 'true' - token: ${{ secrets.GH_BOT_TOKEN }} - - - name: Save preview report - if: >- - always() && !cancelled() && - (steps.preview.outcome == 'success' || steps.preview.outcome == 'failure') - env: - PREVIEW_RESULTS: ${{ steps.preview.outputs.results }} - PREVIEW_OUTCOME: ${{ steps.preview.outcome }} - PREVIEW_CHANGED: ${{ steps.preview.outputs.changed_repositories }} - PREVIEW_FAILED: ${{ steps.preview.outputs.failed_repositories }} - PREVIEW_ORGANIZATION: ${{ inputs.organization }} - PREVIEW_REPOSITORY: ${{ inputs.repository }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('node:path'); - const { pathToFileURL } = require('node:url'); - const script = path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/file-sync-preview.mjs'); - const { writePreviewReport } = await import(pathToFileURL(script).href); - await writePreviewReport({ core, context, env: process.env }); - - - name: Upload preview report - if: >- - always() && !cancelled() && - (steps.preview.outcome == 'success' || steps.preview.outcome == 'failure') - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: file-sync-preview-${{ github.run_id }}-${{ github.run_attempt }} - path: build/file-sync-preview/results.json - if-no-files-found: error - retention-days: 14 diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index 5ae3b2d5..fac246d1 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -1,348 +1,88 @@ --- -# This workflow does not need to be replicated to other repositories. This workflow will update the files in other org -# repos. - +# This workflow runs only in the source .github repository. name: Global replicator permissions: {} on: + pull_request: push: branches: - - master # only files that changed in the commit will be replicated, unless using `workflow_dispatch` + - master + schedule: + - cron: '17 11 * * *' workflow_dispatch: inputs: - repo_name: - description: | - You can specify the repository's name where workflows should be pushed manually, as long as workflow settings - do not ignore the repository. If you do not specify the exact repository name, the workflow will try to - replicate all missing changes to all repositories. + organization: + description: 'Optional organization from organizations.json; leave empty to sync all organizations.' + required: false + type: string + default: '' + repository: + description: 'Optional exact owner/repository; leave empty to sync all configured targets.' required: false + type: string + default: '' + +concurrency: + # All delivery runs share one group, even when dispatched from different refs. + group: global-replicator-${{ github.event_name == 'pull_request' && github.event.pull_request.number || 'delivery' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: replicate: name: Replicate files - env: - BOT_BRANCH_NAME: 'bot/update-files-from-global-repo' - COMMIT_MESSAGE: 'chore: update global workflows' - EXCLUDE_PRIVATE: true + if: >- + github.repository == 'LizardByte/.github' && + (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) permissions: contents: read + env: + SYNC_DRY_RUN: ${{ github.event_name == 'pull_request' }} runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Load repositories to ignore - shell: bash - run: | - set -euo pipefail - - ignore_file=".github/repositories-to-ignore.json" - repos_to_ignore="$( - jq -er ' - if type == "array" and all(.[]; type == "string" and length > 0) - then join(",") - else error("Expected an array of non-empty repository names") - end - ' "${ignore_file}" - )" - echo "REPOS_TO_IGNORE=${repos_to_ignore}" >> "${GITHUB_ENV}" - echo "Loaded $(jq -r 'length' "${ignore_file}") repositories to ignore." - - - name: Removing files - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 - with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_remove: >- - .codeql-prebuild-cpp-Linux.sh, - .codeql-prebuild-cpp-Windows.sh, - .codeql-prebuild-cpp-macOS.sh, - .github/label-actions.yml, - .github/pr_release_template.md, - .github/workflows/_docker.yml, - .github/workflows/auto-create-pr.yml, - .github/workflows/automerge.yml, - .github/workflows/autoupdate.yml, - .github/workflows/autoupdate-labeler.yml, - .github/workflows/ci-docker.yml, - .github/workflows/ci-qodana.yml, - .github/workflows/codeql.yml, - .github/workflows/common-lint.yml, - .github/workflows/cpp-lint.yml, - .github/workflows/dispatcher.yml, - .github/workflows/issues.yml, - .github/workflows/issues-stale.yml, - .github/workflows/pull-requests.yml, - .github/workflows/python-flake8.yml, - .github/workflows/release-notifier.yml, - .github/workflows/top_issues.yml, - .github/workflows/update-changelog.yml, - .github/workflows/update-docs.yml, - .github/workflows/update-flathub-repo.yml, - .github/workflows/update-homebrew-release.yml, - .github/workflows/update-pacman-repo.yml, - .github/workflows/update-winget-release.yml, - .github/workflows/yaml-lint.yml, - codecov.yml, - qodana-dotnet.yaml, - qodana-js.yaml, - qodana-python.yaml - topics_to_include: '' - exclude_forked: false - destination: '' - - # checkout between each step to ensure we have a clean GitHub workspace - # see https://github.com/derberg/manage-files-in-multiple-repositories/issues/61#issuecomment-1747013728 - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Replicating files (general) - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 - with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .github/dependabot.yml, - .github/semantic.yml, - .github/workflows/_codeql.yml, - .github/workflows/_common-lint.yml, - .github/workflows/_top-issues.yml, - renovate.json - topics_to_include: '' - exclude_forked: false - destination: '' - - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Replicating files (docs) - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 - with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .github/workflows/_update-docs.yml - topics_to_include: 'rtd' - exclude_forked: false - destination: '' - - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Replicating files (flathub) - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 - with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .github/workflows/_update-flathub-repo.yml - topics_to_include: 'flathub-pkg' - exclude_forked: false - destination: '' - - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Replicating files (homebrew) - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 - with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .github/workflows/_update-homebrew-repo.yml - topics_to_include: 'homebrew-pkg' - exclude_forked: false - destination: '' - - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Replicating files (pacman) - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 - with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .github/workflows/_update-pacman-repo.yml - topics_to_include: 'pacman-pkg' - exclude_forked: false - destination: '' - - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Replicating files (winget) - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 - with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .github/workflows/_update-winget-repo.yml - topics_to_include: 'winget-pkg' - exclude_forked: false - destination: '' - - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Replicating files (node CI) - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 - with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .github/workflows/_ci-node.yml - topics_to_include: 'nodejs' - exclude_forked: false - destination: '' - - - name: Checkout repository + - name: Checkout source policy and files uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Replicating files (npm) - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .github/workflows/_update-npm.yml - topics_to_include: 'npm-pkg' - exclude_forked: false - destination: '' - - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + persist-credentials: false - - name: Replicating files (python) - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 + - name: Synchronize shared files + id: sync + uses: LizardByte/actions/actions/org_sync@d5471304498e623f670db93062e3769a5bf14192 # org_sync PR #348 with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .flake8 - topics_to_include: 'python' - exclude_forked: false - destination: '' - - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Replicating files (c, cpp) - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 + config: .github/file-sync.json + organizations_file: organizations.json + organization: ${{ inputs.organization }} + repository: ${{ inputs.repository }} + dry_run: ${{ env.SYNC_DRY_RUN }} + token: ${{ secrets.GH_BOT_TOKEN }} + + - name: Save synchronization report + if: >- + always() && !cancelled() && + (steps.sync.outcome == 'success' || steps.sync.outcome == 'failure') + env: + SYNC_RESULTS: ${{ steps.sync.outputs.results }} + SYNC_OUTCOME: ${{ steps.sync.outcome }} + SYNC_CHANGED: ${{ steps.sync.outputs.changed_repositories }} + SYNC_FAILED: ${{ steps.sync.outputs.failed_repositories }} + SYNC_ORGANIZATION: ${{ inputs.organization }} + SYNC_REPOSITORY: ${{ inputs.repository }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .clang-format - topics_to_include: 'c,cpp' - exclude_forked: false - destination: '' - - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Replicating files (custom issues) - # GitHub will not show the global issue config if there are any custom templates in the repo - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 - with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .github/ISSUE_TEMPLATE/config.yml - topics_to_include: 'replicator-custom-issues' - exclude_forked: false - destination: '' - - - name: Checkout repository - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Replicating files (release/changelog workflows) - uses: derberg/manage-files-in-multiple-repositories@b64d9c8480606c15f957cdda078f0122815512b8 # v3.1.2 + script: | + const path = require('node:path'); + const { pathToFileURL } = require('node:url'); + const script = path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/file-sync-report.mjs'); + const { writeSyncReport } = await import(pathToFileURL(script).href); + await writeSyncReport({ core, context, env: process.env }); + + - name: Upload synchronization report + if: >- + always() && !cancelled() && + (steps.sync.outcome == 'success' || steps.sync.outcome == 'failure') + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: - bot_branch_name: ${{ env.BOT_BRANCH_NAME }} - commit_message: ${{ env.COMMIT_MESSAGE }} - exclude_private: ${{ env.EXCLUDE_PRIVATE }} - repos_to_ignore: ${{ env.REPOS_TO_IGNORE }} - github_token: ${{ secrets.GH_BOT_TOKEN }} - committer_username: ${{ vars.GH_BOT_NAME }} - committer_email: ${{ secrets.GH_BOT_EMAIL }} - patterns_to_ignore: '' - patterns_to_include: >- - .github/workflows/_release-notifier.yml, - .github/workflows/_update-changelog.yml - topics_to_include: 'replicator-release-notifications' - exclude_forked: false - destination: '' + name: file-sync-${{ github.run_id }}-${{ github.run_attempt }} + path: build/file-sync/results.json + if-no-files-found: error + retention-days: 14 diff --git a/tests/file-sync-policy.test.mjs b/tests/file-sync-policy.test.mjs index 0cbd12eb..d1aec774 100644 --- a/tests/file-sync-policy.test.mjs +++ b/tests/file-sync-policy.test.mjs @@ -5,41 +5,34 @@ import test from 'node:test'; const policy = JSON.parse(readFileSync('.github/file-sync.json', 'utf8')); const organizations = JSON.parse(readFileSync('organizations.json', 'utf8')); const schema = JSON.parse(readFileSync('repository-properties.json', 'utf8')); -const legacy = readFileSync('.github/workflows/__global-replicator.yml', 'utf8'); -const preview = readFileSync('.github/workflows/__file-sync-preview.yml', 'utf8'); +const workflow = readFileSync('.github/workflows/__global-replicator.yml', 'utf8'); const compareText = (left, right) => left.localeCompare(right); -const profiles = new Map([ - ['', undefined], ['rtd', 'docs'], ['flathub-pkg', 'flathub'], ['homebrew-pkg', 'homebrew'], - ['pacman-pkg', 'pacman'], ['winget-pkg', 'winget'], ['nodejs', 'node_ci'], ['npm-pkg', 'npm'], - ['python', 'python_style'], ['c,cpp', 'cpp_style'], ['replicator-custom-issues', 'custom_issues'], - ['replicator-release-notifications', 'release_notifications'], -]); -test('file policy preserves every active legacy file group and its profile selection', () => { - const groups = [...legacy.matchAll(/patterns_to_include: >-\r?\n([\s\S]*?)\r?\n\s+topics_to_include: '([^']*)'/g)]; - assert.equal(groups.length, 12); - for (const [, sourceList, topics] of groups) { - assert.ok(profiles.has(topics), `Unmapped legacy selector: ${topics}`); - const profile = profiles.get(topics); - const rules = policy.rules.filter(rule => rule.profile === profile && !rule.repositories); - for (const source of sourceList.trim().split(/,\s*/)) { - const mapping = rules.flatMap(rule => rule.files).find(file => file.source === source); - assert.ok(mapping, `Missing legacy source: ${source}`); - assert.equal(mapping.destination ?? mapping.source, source); - assert.equal(mapping.mode ?? 'sync', 'sync'); - } +test('general files apply without profiles and each optional group has its own property selection', () => { + const general = policy.rules.find(rule => rule.id === 'general'); + assert.equal(general.profile, undefined); + assert.equal(general.repositories, undefined); + assert.ok(general.files.some(file => file.source === 'renovate.json')); + const profiles = policy.rules.filter(rule => rule.profile); + assert.equal(profiles.length, 11); + for (const rule of profiles) { + assert.equal(rule.profile, rule.id); + assert.equal(rule.repositories, undefined); + } + for (const file of policy.rules.flatMap(rule => rule.files).filter(file => file.source)) { + assert.equal(file.destination ?? file.source, file.source); + assert.equal(file.mode ?? 'sync', 'sync'); } }); test('cleanup remains explicit and the obsolete Docker wrapper is never a source', () => { - const cleanup = legacy.match(/patterns_to_remove: >-\r?\n([\s\S]*?)\r?\n\s+topics_to_include:/)[1]; - const expected = cleanup.trim().split(/,\s*/); const actual = policy.rules.flatMap(rule => rule.files).filter(file => file.mode === 'remove'); - assert.deepEqual(actual.map(file => file.destination).toSorted(compareText), expected.toSorted(compareText)); + assert.equal(actual.length, 33); + assert.equal(new Set(actual.map(file => file.destination)).size, actual.length); assert.ok(actual.some(file => file.destination === '.github/workflows/_docker.yml')); assert.ok(actual.every(file => file.source === undefined)); - assert.ok(policy.rules.flatMap(rule => rule.files).every(file => file.source !== '.github/workflows/_docker.yml')); - assert.doesNotMatch(legacy, /name: Replicating files \(docker\)/); + const sources = policy.rules.flatMap(rule => rule.files).filter(file => file.source).map(file => file.source); + assert.ok(actual.every(file => !sources.includes(file.destination))); }); test('configured sources exist and policy selectors use the shared organization list and property schema', () => { @@ -78,15 +71,24 @@ test('PR templates target only exact organization .github repositories', () => { assert.equal(policy.include_forks, true); }); -test('preview cannot enable delivery and the legacy writer stays enabled', () => { - const dispatch = preview.slice(preview.indexOf(' workflow_dispatch:'), preview.indexOf('\nconcurrency:')); - assert.doesNotMatch(dispatch, /dry_run|type: boolean/); - assert.match(preview, /dry_run: 'true'/); - assert.doesNotMatch(preview, /dry_run:.*\$\{\{/); - assert.match(preview, /LizardByte\/actions\/actions\/org_sync@d5471304498e623f670db93062e3769a5bf14192/); - assert.match(preview, /github\.event\.pull_request\.head\.repo\.full_name == github\.repository/); - assert.doesNotMatch(preview, /pull_request_target:|contents: write|pull-requests: write|continue-on-error:/); - assert.match(legacy, / push:\r?\n branches:\r?\n - master/); - assert.match(legacy, / workflow_dispatch:/); - assert.match(legacy, /uses: derberg\/manage-files-in-multiple-repositories@/); +test('all PR and master changes run, PRs preview, and production runs are serialized without cancellation', () => { + const events = workflow.slice(workflow.indexOf('\non:'), workflow.indexOf('\nconcurrency:')); + assert.match(events, / pull_request:/); + assert.match(events, / push:\r?\n branches:\r?\n - master/); + assert.doesNotMatch(events, /paths:|paths-ignore:/); + assert.match(workflow, /SYNC_DRY_RUN: \$\{\{ github\.event_name == 'pull_request' \}\}/); + assert.match(workflow, /dry_run: \$\{\{ env\.SYNC_DRY_RUN \}\}/); + assert.match(workflow, /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/); + assert.match(workflow, + /group: global-replicator-\$\{\{ github\.event_name == 'pull_request' && github\.event\.pull_request\.number \|\| 'delivery' \}\}/); + assert.doesNotMatch(events, /dry_run|repo_name/); +}); + +test('only the pinned new action runs and credentialed PRs retain their fork guard', () => { + assert.match(workflow, /LizardByte\/actions\/actions\/org_sync@d5471304498e623f670db93062e3769a5bf14192/); + assert.match(workflow, /github\.event\.pull_request\.head\.repo\.full_name == github\.repository/); + assert.doesNotMatch(workflow, /pull_request_target:|contents: write|pull-requests: write|continue-on-error:/); + assert.match(workflow, /persist-credentials: false/); + assert.doesNotMatch(workflow, /derberg\/|topics_to_include|repositories-to-ignore/); + assert.equal(existsSync('.github/workflows/__file-sync-preview.yml'), false); }); diff --git a/tests/file-sync-preview.test.mjs b/tests/file-sync-report.test.mjs similarity index 53% rename from tests/file-sync-preview.test.mjs rename to tests/file-sync-report.test.mjs index 27590bed..21d85411 100644 --- a/tests/file-sync-preview.test.mjs +++ b/tests/file-sync-report.test.mjs @@ -4,7 +4,7 @@ import os from 'node:os'; import path from 'node:path'; import test from 'node:test'; -import {writePreviewReport} from '../.github/scripts/file-sync-preview.mjs'; +import {writeSyncReport} from '../.github/scripts/file-sync-report.mjs'; function summary() { const calls = []; @@ -26,10 +26,10 @@ test('partial discovery failures retain exact changes and errors in the read-onl {repository: 'other/*', status: 'failed', error: 'Property access denied'}, {repository: 'example/mirror', status: 'excluded'}, ]; - const report = await writePreviewReport({ + const report = await writeSyncReport({ core: {summary: output}, context: {sha: 'revision', ref: 'refs/pull/1/merge'}, outputDirectory: directory, - env: {PREVIEW_RESULTS: JSON.stringify(results), PREVIEW_OUTCOME: 'failure', PREVIEW_CHANGED: '1', PREVIEW_FAILED: '1', - PREVIEW_ORGANIZATION: 'example', PREVIEW_REPOSITORY: 'example/app'}, + env: {SYNC_DRY_RUN: 'true', SYNC_RESULTS: JSON.stringify(results), SYNC_OUTCOME: 'failure', + SYNC_CHANGED: '1', SYNC_FAILED: '1', SYNC_ORGANIZATION: 'example', SYNC_REPOSITORY: 'example/app'}, }); assert.equal(report.dry_run, true); assert.equal(report.outcome, 'failure'); @@ -44,17 +44,18 @@ test('partial discovery failures retain exact changes and errors in the read-onl assert.deepEqual(output.calls.find(([type]) => type === 'table')[1].slice(1), [ ['example/app', 'planned', '1'], ['other/*', 'failed', '0'], ['example/mirror', 'excluded', '0'], ]); - assert.ok(output.calls.some(([type, value]) => type === 'raw' && value.includes('global replicator remains active'))); + assert.ok(output.calls.some(([type, value]) => type === 'raw' && value.includes('Dry run only'))); + assert.ok(output.calls.some(([type, value]) => type === 'heading' && value === 'File sync preview')); assert.deepEqual(output.calls.at(-1), ['write']); }); -test('an action failure without outputs still produces a diagnostic report', async t => { +test('an action failure without outputs still produces a delivery diagnostic report', async t => { const directory = await mkdtemp(path.join(os.tmpdir(), 'file-sync-report-')); t.after(() => rm(directory, {recursive: true, force: true})); const output = summary(); - const report = await writePreviewReport({ + const report = await writeSyncReport({ core: {summary: output}, context: {sha: 'revision', ref: 'refs/heads/master'}, - env: {PREVIEW_OUTCOME: 'failure'}, outputDirectory: directory, + env: {SYNC_DRY_RUN: 'false', SYNC_OUTCOME: 'failure'}, outputDirectory: directory, }); assert.deepEqual(report.results, []); assert.equal(report.changed_repositories, 0); @@ -62,12 +63,41 @@ test('an action failure without outputs still produces a diagnostic report', asy assert.equal(report.organization, null); assert.equal(report.repository, null); assert.ok(output.calls.some(([type, value]) => type === 'raw' && value.includes('**failure**'))); - assert.equal(JSON.parse(await readFile(path.join(directory, 'results.json'), 'utf8')).dry_run, true); + assert.equal(JSON.parse(await readFile(path.join(directory, 'results.json'), 'utf8')).dry_run, false); +}); + +test('delivery reports retain target PR URLs and never claim to be read-only', async t => { + const directory = await mkdtemp(path.join(os.tmpdir(), 'file-sync-report-')); + t.after(() => rm(directory, {recursive: true, force: true})); + const output = summary(); + const results = [{repository: 'example/app', status: 'updated', + pull_request: 'https://github.com/example/app/pull/42', changes: [{path: 'shared.yml', operation: 'update'}]}]; + const report = await writeSyncReport({ + core: {summary: output}, context: {sha: 'revision', ref: 'refs/heads/master'}, outputDirectory: directory, + env: {SYNC_DRY_RUN: 'false', SYNC_RESULTS: JSON.stringify(results), SYNC_OUTCOME: 'success', SYNC_CHANGED: '1'}, + }); + assert.equal(report.dry_run, false); + assert.equal(report.changed_repositories, 1); + assert.deepEqual(JSON.parse(await readFile(path.join(directory, 'results.json'), 'utf8')).results, results); + assert.ok(output.calls.some(([type, value]) => type === 'heading' && value === 'File synchronization')); + const prose = output.calls.filter(([type]) => type === 'raw').map(([, value]) => value).join(''); + assert.match(prose, /Delivery enabled/); + assert.doesNotMatch(prose, /Dry run only|No target/); }); test('malformed action output is rejected before writing a misleading summary', async () => { const output = summary(); - await assert.rejects(writePreviewReport({core: {summary: output}, context: {}, env: {PREVIEW_RESULTS: '{}'}}), + await assert.rejects(writeSyncReport({core: {summary: output}, context: {}, + env: {SYNC_DRY_RUN: 'true', SYNC_RESULTS: '{}'}}), /Expected an array of file sync results/); assert.deepEqual(output.calls, []); }); + +test('missing or invalid mode cannot produce a misleading read-only report', async () => { + for (const mode of [undefined, '', 'FALSE', '0']) { + const output = summary(); + await assert.rejects(writeSyncReport({core: {summary: output}, context: {}, env: {SYNC_DRY_RUN: mode}}), + /Expected an explicit file sync dry-run mode/); + assert.deepEqual(output.calls, []); + } +}); From 8a7844b066a11fd0e310860d7c413720f2d0d15f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:06:12 -0500 Subject: [PATCH 03/22] ci: use reusable sync reports and repository name targets --- .github/file-sync.json | 6 +- .github/file-sync.md | 21 +++-- .github/scripts/file-sync-report.mjs | 50 ----------- .github/workflows/__global-replicator.yml | 39 +------- organizations.json | 4 +- tests/file-sync-policy.test.mjs | 15 ++-- tests/file-sync-report.test.mjs | 103 ---------------------- 7 files changed, 29 insertions(+), 209 deletions(-) delete mode 100644 .github/scripts/file-sync-report.mjs delete mode 100644 tests/file-sync-report.test.mjs diff --git a/.github/file-sync.json b/.github/file-sync.json index f836fe71..14faeaf4 100644 --- a/.github/file-sync.json +++ b/.github/file-sync.json @@ -270,10 +270,8 @@ }, { "id": "organization-pr-template", - "repositories": [ - "LizardByte/.github", - "LizardByte-infrastructure/.github", - "SunshineStream/.github" + "repository_names": [ + ".github" ], "files": [ { diff --git a/.github/file-sync.md b/.github/file-sync.md index a64a7554..423dbc13 100644 --- a/.github/file-sync.md +++ b/.github/file-sync.md @@ -8,7 +8,7 @@ and the separate preview workflow. A single checkout supplies the policy and sou | ------------------------ | -------- | ------------------------------------------ | | Internal pull request | Dry run | All configured targets | | Push to master | Delivery | All configured targets | -| Daily at 11:17 UTC | Delivery | All configured targets | +| Daily at midnight UTC | Delivery | All configured targets | | Manual workflow dispatch | Delivery | All targets or the selected org/repository | Every pull request and master push runs the workflow regardless of which files changed. Fork pull requests @@ -28,22 +28,27 @@ take over branches or PRs created by the old replicator. Renovate, use `sync`. `create_if_missing` is available for reviewed exceptions. - [repository-properties.json](../repository-properties.json) records the definitions provisioned in each org. `sync_mode: excluded` skips a repository. Multi-select `workflow_profiles` selects additional file groups. + GitHub does not apply this JSON automatically, and the file-sync action does not read it. Schema definitions + were applied separately through the GitHub API. Updating this file alone does not change live properties; + provisioning/synchronizing schema definitions remains a separate administrative operation. - Public, unarchived repositories are eligible by default, including forks. The source repository is skipped. Existing mirror and Upptime exclusions in LizardByte-infrastructure apply through their properties. - Deselected profiles remove their configured files on the next run. General files remain, and another selected rule can retain a shared path. Missing sources fail validation; deletion requires a profile/policy change or explicit cleanup rule. `_docker.yml` is cleanup-only because its source no longer exists. -- PR templates target only the existing `LizardByte/.github`, `LizardByte-infrastructure/.github`, and - `SunshineStream/.github` repositories. Add `Themerr/.github` and `KokoStreamTV/.github` after creating them. +- PR templates use `repository_names: [".github"]` across `organizations.json`. Adding an org or creating its + `.github` repo needs no policy edit. Organizations without a `.github` repo are skipped for this rule. + Normal eligibility and custom-property checks still apply, including the source-repository exclusion. - The `docker` and `cpm` profile choices currently have no file mappings. CPM maintenance is a separate workflow. ## Reviewing a run -The run summary states whether delivery or dry-run mode was used and lists each repository's status and file -change count. Download `file-sync--` and inspect `results.json` for exact paths, operations, -warnings, errors, and target PR URLs. Partial results are saved even when another target fails; a failed sync -still fails the job. Private/archived repositories, the skipped source repository, and empty organizations -produce no result rows. +The reusable action writes the run summary and uploads a JSON report artifact with a unique `file-sync-` name. +No local report script or upload step is needed. The summary states whether delivery or dry-run mode was used +and lists each repository's status and file change count. Inspect the artifact's `results.json` for exact paths, +operations, warnings, errors, and target PR URLs. Partial results are saved even when another target fails; a +failed sync still fails the job. Private/archived repositories, the skipped source repository, and empty +organizations produce no result rows. The existing `GH_BOT_TOKEN` must read repository metadata/contents and organization custom properties across the listed orgs. Delivery additionally requires permission to write contents/workflows and manage target PRs. diff --git a/.github/scripts/file-sync-report.mjs b/.github/scripts/file-sync-report.mjs deleted file mode 100644 index 126d063f..00000000 --- a/.github/scripts/file-sync-report.mjs +++ /dev/null @@ -1,50 +0,0 @@ -import {mkdir, writeFile} from 'node:fs/promises'; -import path from 'node:path'; - -/** - * Save preview or delivery results and publish their counts to the run summary. - * Failed action steps can still provide partial results for the other organizations. - * - * @param {object} options Actions core/context, environment, and optional output directory. - * @returns {Promise} The report written to the artifact. - */ -export async function writeSyncReport({core, context, env, outputDirectory = 'build/file-sync'}) { - if (!['true', 'false'].includes(env.SYNC_DRY_RUN)) { - throw new TypeError('Expected an explicit file sync dry-run mode'); - } - const results = JSON.parse(env.SYNC_RESULTS || '[]'); - if (!Array.isArray(results)) { - throw new TypeError('Expected an array of file sync results'); - } - const report = { - dry_run: env.SYNC_DRY_RUN === 'true', - source_commit: context.sha, - source_ref: context.ref, - organization: env.SYNC_ORGANIZATION || null, - repository: env.SYNC_REPOSITORY || null, - outcome: env.SYNC_OUTCOME, - changed_repositories: Number(env.SYNC_CHANGED || 0), - failed_repositories: Number(env.SYNC_FAILED || 0), - results, - }; - await mkdir(outputDirectory, {recursive: true}); - await writeFile(path.join(outputDirectory, 'results.json'), `${JSON.stringify(report, null, 2)}\n`, 'utf8'); - core.summary.addHeading(report.dry_run ? 'File sync preview' : 'File synchronization'); - core.summary.addRaw(report.dry_run - ? 'Dry run only. No target Git objects, branches, or pull requests were written.\n\n' - : 'Delivery enabled. Shared-file changes are proposed through target pull requests.\n\n'); - core.summary.addRaw(`Sync step outcome: **${report.outcome}**. See the step logs for failures.\n\n`); - core.summary.addRaw(`Repositories changed or planned: **${report.changed_repositories}**; ` - + `failed targets: **${report.failed_repositories}**.\n\n`); - core.summary.addTable([ - [ - {data: 'Repository', header: true}, - {data: 'Status', header: true}, - {data: 'File changes', header: true}, - ], - ...results.map(result => [result.repository, result.status, String(result.changes?.length ?? 0)]), - ]); - core.summary.addRaw('\nDownload the file-sync artifact for exact paths, operations, warnings, errors, and PR URLs.\n'); - await core.summary.write(); - return report; -} diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index fac246d1..a1a09ae8 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -9,7 +9,7 @@ on: branches: - master schedule: - - cron: '17 11 * * *' + - cron: '0 0 * * *' # Daily at midnight UTC. workflow_dispatch: inputs: organization: @@ -36,8 +36,6 @@ jobs: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) permissions: contents: read - env: - SYNC_DRY_RUN: ${{ github.event_name == 'pull_request' }} runs-on: ubuntu-latest steps: - name: Checkout source policy and files @@ -47,42 +45,11 @@ jobs: - name: Synchronize shared files id: sync - uses: LizardByte/actions/actions/org_sync@d5471304498e623f670db93062e3769a5bf14192 # org_sync PR #348 + uses: LizardByte/actions/actions/org_sync@c4828dc659fd8e381db3ace5766cf6470961eb85 # org_sync PR #351 with: config: .github/file-sync.json organizations_file: organizations.json organization: ${{ inputs.organization }} repository: ${{ inputs.repository }} - dry_run: ${{ env.SYNC_DRY_RUN }} + dry_run: ${{ github.event_name == 'pull_request' }} token: ${{ secrets.GH_BOT_TOKEN }} - - - name: Save synchronization report - if: >- - always() && !cancelled() && - (steps.sync.outcome == 'success' || steps.sync.outcome == 'failure') - env: - SYNC_RESULTS: ${{ steps.sync.outputs.results }} - SYNC_OUTCOME: ${{ steps.sync.outcome }} - SYNC_CHANGED: ${{ steps.sync.outputs.changed_repositories }} - SYNC_FAILED: ${{ steps.sync.outputs.failed_repositories }} - SYNC_ORGANIZATION: ${{ inputs.organization }} - SYNC_REPOSITORY: ${{ inputs.repository }} - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 - with: - script: | - const path = require('node:path'); - const { pathToFileURL } = require('node:url'); - const script = path.join(process.env.GITHUB_WORKSPACE, '.github/scripts/file-sync-report.mjs'); - const { writeSyncReport } = await import(pathToFileURL(script).href); - await writeSyncReport({ core, context, env: process.env }); - - - name: Upload synchronization report - if: >- - always() && !cancelled() && - (steps.sync.outcome == 'success' || steps.sync.outcome == 'failure') - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 - with: - name: file-sync-${{ github.run_id }}-${{ github.run_attempt }} - path: build/file-sync/results.json - if-no-files-found: error - retention-days: 14 diff --git a/organizations.json b/organizations.json index 50b53439..267f9b57 100644 --- a/organizations.json +++ b/organizations.json @@ -1,7 +1,7 @@ [ + "KokoStreamTV", "LizardByte", "LizardByte-infrastructure", "SunshineStream", - "Themerr", - "KokoStreamTV" + "Themerr" ] diff --git a/tests/file-sync-policy.test.mjs b/tests/file-sync-policy.test.mjs index d1aec774..11228c3c 100644 --- a/tests/file-sync-policy.test.mjs +++ b/tests/file-sync-policy.test.mjs @@ -39,6 +39,7 @@ test('configured sources exist and policy selectors use the shared organization assert.equal(policy.version, 1); assert.ok(organizations.every(org => typeof org === 'string')); assert.equal(new Set(organizations).size, organizations.length); + assert.deepEqual(organizations, organizations.toSorted(compareText)); const profileSchema = schema.properties.find(property => property.property_name === 'workflow_profiles'); for (const rule of policy.rules) { if (rule.profile) { @@ -59,11 +60,12 @@ test('configured sources exist and policy selectors use the shared organization assert.deepEqual(profileSchema.allowed_values, profileSchema.allowed_values.toSorted(compareText)); }); -test('PR templates target only exact organization .github repositories', () => { +test('PR templates target .github by name across the shared organization list', () => { const templates = policy.rules.filter(rule => rule.files.some(file => file.source === '.github/pull_request_template.md')); assert.equal(templates.length, 1); - assert.ok(templates[0].repositories.length > 0); - assert.ok(templates[0].repositories.every(repo => repo.endsWith('/.github'))); + assert.deepEqual(templates[0].repository_names, ['.github']); + assert.equal(templates[0].repositories, undefined); + assert.equal(templates[0].organizations, undefined); assert.equal(templates[0].profile, undefined); assert.equal(policy.include_source, false); assert.equal(policy.include_private, false); @@ -76,8 +78,7 @@ test('all PR and master changes run, PRs preview, and production runs are serial assert.match(events, / pull_request:/); assert.match(events, / push:\r?\n branches:\r?\n - master/); assert.doesNotMatch(events, /paths:|paths-ignore:/); - assert.match(workflow, /SYNC_DRY_RUN: \$\{\{ github\.event_name == 'pull_request' \}\}/); - assert.match(workflow, /dry_run: \$\{\{ env\.SYNC_DRY_RUN \}\}/); + assert.match(workflow, /dry_run: \$\{\{ github\.event_name == 'pull_request' \}\}/); assert.match(workflow, /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/); assert.match(workflow, /group: global-replicator-\$\{\{ github\.event_name == 'pull_request' && github\.event\.pull_request\.number \|\| 'delivery' \}\}/); @@ -85,10 +86,12 @@ test('all PR and master changes run, PRs preview, and production runs are serial }); test('only the pinned new action runs and credentialed PRs retain their fork guard', () => { - assert.match(workflow, /LizardByte\/actions\/actions\/org_sync@d5471304498e623f670db93062e3769a5bf14192/); + assert.match(workflow, /LizardByte\/actions\/actions\/org_sync@[a-f\d]{40}/); assert.match(workflow, /github\.event\.pull_request\.head\.repo\.full_name == github\.repository/); assert.doesNotMatch(workflow, /pull_request_target:|contents: write|pull-requests: write|continue-on-error:/); assert.match(workflow, /persist-credentials: false/); assert.doesNotMatch(workflow, /derberg\/|topics_to_include|repositories-to-ignore/); assert.equal(existsSync('.github/workflows/__file-sync-preview.yml'), false); + assert.equal(existsSync('.github/scripts/file-sync-report.mjs'), false); + assert.doesNotMatch(workflow, /uses: actions\/upload-artifact|Save synchronization report/); }); diff --git a/tests/file-sync-report.test.mjs b/tests/file-sync-report.test.mjs deleted file mode 100644 index 21d85411..00000000 --- a/tests/file-sync-report.test.mjs +++ /dev/null @@ -1,103 +0,0 @@ -import assert from 'node:assert/strict'; -import {mkdtemp, readFile, rm} from 'node:fs/promises'; -import os from 'node:os'; -import path from 'node:path'; -import test from 'node:test'; - -import {writeSyncReport} from '../.github/scripts/file-sync-report.mjs'; - -function summary() { - const calls = []; - return { - calls, - addHeading: value => calls.push(['heading', value]), - addRaw: value => calls.push(['raw', value]), - addTable: value => calls.push(['table', value]), - write: async () => calls.push(['write']), - }; -} - -test('partial discovery failures retain exact changes and errors in the read-only artifact', async t => { - const directory = await mkdtemp(path.join(os.tmpdir(), 'file-sync-report-')); - t.after(() => rm(directory, {recursive: true, force: true})); - const output = summary(); - const results = [ - {repository: 'example/app', status: 'planned', changes: [{path: 'old.yml', operation: 'remove'}], warnings: []}, - {repository: 'other/*', status: 'failed', error: 'Property access denied'}, - {repository: 'example/mirror', status: 'excluded'}, - ]; - const report = await writeSyncReport({ - core: {summary: output}, context: {sha: 'revision', ref: 'refs/pull/1/merge'}, outputDirectory: directory, - env: {SYNC_DRY_RUN: 'true', SYNC_RESULTS: JSON.stringify(results), SYNC_OUTCOME: 'failure', - SYNC_CHANGED: '1', SYNC_FAILED: '1', SYNC_ORGANIZATION: 'example', SYNC_REPOSITORY: 'example/app'}, - }); - assert.equal(report.dry_run, true); - assert.equal(report.outcome, 'failure'); - assert.equal(report.source_commit, 'revision'); - assert.equal(report.source_ref, 'refs/pull/1/merge'); - assert.equal(report.organization, 'example'); - assert.equal(report.repository, 'example/app'); - assert.equal(report.changed_repositories, 1); - assert.equal(report.failed_repositories, 1); - assert.deepEqual(report.results, results); - assert.deepEqual(JSON.parse(await readFile(path.join(directory, 'results.json'), 'utf8')), report); - assert.deepEqual(output.calls.find(([type]) => type === 'table')[1].slice(1), [ - ['example/app', 'planned', '1'], ['other/*', 'failed', '0'], ['example/mirror', 'excluded', '0'], - ]); - assert.ok(output.calls.some(([type, value]) => type === 'raw' && value.includes('Dry run only'))); - assert.ok(output.calls.some(([type, value]) => type === 'heading' && value === 'File sync preview')); - assert.deepEqual(output.calls.at(-1), ['write']); -}); - -test('an action failure without outputs still produces a delivery diagnostic report', async t => { - const directory = await mkdtemp(path.join(os.tmpdir(), 'file-sync-report-')); - t.after(() => rm(directory, {recursive: true, force: true})); - const output = summary(); - const report = await writeSyncReport({ - core: {summary: output}, context: {sha: 'revision', ref: 'refs/heads/master'}, - env: {SYNC_DRY_RUN: 'false', SYNC_OUTCOME: 'failure'}, outputDirectory: directory, - }); - assert.deepEqual(report.results, []); - assert.equal(report.changed_repositories, 0); - assert.equal(report.failed_repositories, 0); - assert.equal(report.organization, null); - assert.equal(report.repository, null); - assert.ok(output.calls.some(([type, value]) => type === 'raw' && value.includes('**failure**'))); - assert.equal(JSON.parse(await readFile(path.join(directory, 'results.json'), 'utf8')).dry_run, false); -}); - -test('delivery reports retain target PR URLs and never claim to be read-only', async t => { - const directory = await mkdtemp(path.join(os.tmpdir(), 'file-sync-report-')); - t.after(() => rm(directory, {recursive: true, force: true})); - const output = summary(); - const results = [{repository: 'example/app', status: 'updated', - pull_request: 'https://github.com/example/app/pull/42', changes: [{path: 'shared.yml', operation: 'update'}]}]; - const report = await writeSyncReport({ - core: {summary: output}, context: {sha: 'revision', ref: 'refs/heads/master'}, outputDirectory: directory, - env: {SYNC_DRY_RUN: 'false', SYNC_RESULTS: JSON.stringify(results), SYNC_OUTCOME: 'success', SYNC_CHANGED: '1'}, - }); - assert.equal(report.dry_run, false); - assert.equal(report.changed_repositories, 1); - assert.deepEqual(JSON.parse(await readFile(path.join(directory, 'results.json'), 'utf8')).results, results); - assert.ok(output.calls.some(([type, value]) => type === 'heading' && value === 'File synchronization')); - const prose = output.calls.filter(([type]) => type === 'raw').map(([, value]) => value).join(''); - assert.match(prose, /Delivery enabled/); - assert.doesNotMatch(prose, /Dry run only|No target/); -}); - -test('malformed action output is rejected before writing a misleading summary', async () => { - const output = summary(); - await assert.rejects(writeSyncReport({core: {summary: output}, context: {}, - env: {SYNC_DRY_RUN: 'true', SYNC_RESULTS: '{}'}}), - /Expected an array of file sync results/); - assert.deepEqual(output.calls, []); -}); - -test('missing or invalid mode cannot produce a misleading read-only report', async () => { - for (const mode of [undefined, '', 'FALSE', '0']) { - const output = summary(); - await assert.rejects(writeSyncReport({core: {summary: output}, context: {}, env: {SYNC_DRY_RUN: mode}}), - /Expected an explicit file sync dry-run mode/); - assert.deepEqual(output.calls, []); - } -}); From 05c061ef51e5bbaaba14d1fe35cd6d2927e0c95f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:35:56 -0500 Subject: [PATCH 04/22] ci: replace workflow topic checks with custom properties --- .github/workflows/README.md | 6 + .github/workflows/__call-release-notifier.yml | 34 +++-- .../workflows/__call-update-flathub-repo.yml | 44 +++--- .../workflows/__call-update-homebrew-repo.yml | 28 ++-- .../workflows/__call-update-pacman-repo.yml | 38 +++--- .../workflows/__call-update-winget-repo.yml | 32 ++--- .github/workflows/__global-replicator.yml | 2 +- .github/workflows/__update-cpm-cmake.yml | 18 ++- .github/workflows/_ci-node.yml | 2 +- .github/workflows/_update-docs.yml | 2 +- .github/workflows/_update-flathub-repo.yml | 2 +- .github/workflows/_update-homebrew-repo.yml | 2 +- .github/workflows/_update-npm.yml | 2 +- .github/workflows/_update-pacman-repo.yml | 2 +- .github/workflows/_update-winget-repo.yml | 2 +- tests/workflow-profile-selection.test.mjs | 129 ++++++++++++++++++ 16 files changed, 238 insertions(+), 107 deletions(-) create mode 100644 tests/workflow-profile-selection.test.mjs diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 5d933c87..91ca55c8 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -13,6 +13,12 @@ that the workflow should not be modified when inside the other repositories. The [global replicator](../file-sync.md) selects repositories through the `sync_mode` and `workflow_profiles` custom properties. It previews every internal PR and delivers on master pushes, daily, or manually. +Release-notification and package-publishing workflows check the caller's `workflow_profiles` custom property +through the repository API. Their profiles are `release_notifications`, `flathub`, `homebrew`, `pacman`, and +`winget`. The CPM maintenance matrix selects `cpm` through the same property. Missing, null, or empty profile +selections disable that workflow group; invalid values or failed property reads fail the check. These runtime +checks are independent of the file replicator's `sync_mode` setting. + Repositories excluded from the GitLab mirror are listed in `.github/repositories-to-ignore.json`. The GitLab mirror also excludes GitHub temporary security advisory forks matching diff --git a/.github/workflows/__call-release-notifier.yml b/.github/workflows/__call-release-notifier.yml index cce5205e..2b565c41 100644 --- a/.github/workflows/__call-release-notifier.yml +++ b/.github/workflows/__call-release-notifier.yml @@ -30,30 +30,28 @@ jobs: env: BLOG_REPO: ${{ github.repository_owner }}/${{ github.repository_owner }}.github.io steps: - - name: Check topics + - name: Check workflow profile env: - TOPIC: replicator-release-notifications - id: check-label + WORKFLOW_PROFILE: release_notifications + id: check-profile uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const topic = process.env.TOPIC; - console.log(`Checking if repo has topic: ${topic}`); - - const repoTopics = await github.rest.repos.getAllTopics({ + const { data: values } = await github.request('GET /repos/{owner}/{repo}/properties/values', { owner: context.repo.owner, repo: context.repo.repo }); - console.log(`Repo topics: ${repoTopics.data.names}`); - - const hasTopic = repoTopics.data.names.includes(topic); - console.log(`Has topic: ${hasTopic}`); - - core.setOutput('hasTopic', hasTopic); + const profiles = values.find(property => property.property_name === 'workflow_profiles')?.value ?? []; + if (!Array.isArray(profiles) || !profiles.every(profile => typeof profile === 'string')) { + throw new Error('Invalid workflow_profiles custom property'); + } + const enabled = profiles.includes(process.env.WORKFLOW_PROFILE); + core.info(`Workflow profile ${process.env.WORKFLOW_PROFILE}: ${enabled}`); + core.setOutput('enabled', enabled); - name: Check if latest GitHub release id: check-release - if: steps.check-label.outputs.hasTopic == 'true' + if: steps.check-profile.outputs.enabled == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | @@ -66,7 +64,7 @@ jobs: - name: Checkout blog if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -74,7 +72,7 @@ jobs: - name: Create blog post if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' # Keep event data out of the generated shell script so Markdown is never evaluated as shell syntax. env: @@ -125,7 +123,7 @@ jobs: - name: Create/Update Pull Request id: create-pr if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 with: @@ -146,7 +144,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GH_TOKEN }} if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' run: | gh pr merge \ diff --git a/.github/workflows/__call-update-flathub-repo.yml b/.github/workflows/__call-update-flathub-repo.yml index bc4e1ec9..082e4b55 100644 --- a/.github/workflows/__call-update-flathub-repo.yml +++ b/.github/workflows/__call-update-flathub-repo.yml @@ -28,30 +28,28 @@ jobs: contents: read runs-on: ubuntu-latest steps: - - name: Check if Flathub repo - id: check-label + - name: Check workflow profile + id: check-profile env: - TOPIC: flathub-pkg + WORKFLOW_PROFILE: flathub uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const topic = process.env.TOPIC; - console.log(`Checking if repo has topic: ${topic}`); - - const repoTopics = await github.rest.repos.getAllTopics({ + const { data: values } = await github.request('GET /repos/{owner}/{repo}/properties/values', { owner: context.repo.owner, repo: context.repo.repo }); - console.log(`Repo topics: ${repoTopics.data.names}`); - - const hasTopic = repoTopics.data.names.includes(topic); - console.log(`Has topic: ${hasTopic}`); - - core.setOutput('hasTopic', hasTopic); + const profiles = values.find(property => property.property_name === 'workflow_profiles')?.value ?? []; + if (!Array.isArray(profiles) || !profiles.every(profile => typeof profile === 'string')) { + throw new Error('Invalid workflow_profiles custom property'); + } + const enabled = profiles.includes(process.env.WORKFLOW_PROFILE); + core.info(`Workflow profile ${process.env.WORKFLOW_PROFILE}: ${enabled}`); + core.setOutput('enabled', enabled); - name: Check if latest GitHub release id: check-release - if: steps.check-label.outputs.hasTopic == 'true' + if: steps.check-profile.outputs.enabled == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | @@ -64,13 +62,13 @@ jobs: - name: Checkout if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Checkout flathub-repo if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -79,7 +77,7 @@ jobs: - name: Clean up legacy files if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' working-directory: flathub/${{ env.FLATHUB_PKG }} run: | @@ -88,7 +86,7 @@ jobs: - name: Download release asset id: download if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' uses: robinraju/release-downloader@28fc21f50d76778e7023361aa1f863e717d3d56f # v1.13 with: @@ -102,14 +100,14 @@ jobs: - name: Delete archive if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' run: | rm -f flathub/${{ env.FLATHUB_PKG }}/flathub.tar.gz - name: Copy github files if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' working-directory: flathub/${{ env.FLATHUB_PKG }} run: | @@ -139,7 +137,7 @@ jobs: - name: Check if shared-modules submodule exists id: check-submodule if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' env: SUBMODULE_PATH: packaging/linux/flatpak/deps/shared-modules @@ -154,7 +152,7 @@ jobs: - name: Update submodule if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' && steps.check-submodule.outputs.exists == 'true' run: | @@ -172,7 +170,7 @@ jobs: - name: Create/Update Pull Request id: create-pr if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' && fromJson(steps.download.outputs.downloaded_files)[0] uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 diff --git a/.github/workflows/__call-update-homebrew-repo.yml b/.github/workflows/__call-update-homebrew-repo.yml index e6010816..cd614fd5 100644 --- a/.github/workflows/__call-update-homebrew-repo.yml +++ b/.github/workflows/__call-update-homebrew-repo.yml @@ -26,30 +26,28 @@ jobs: name: Homebrew url: https://github.com/${{ github.repository_owner }}/homebrew-homebrew/commits steps: - - name: Check if Homebrew repo - id: check-label + - name: Check workflow profile + id: check-profile env: - TOPIC: homebrew-pkg + WORKFLOW_PROFILE: homebrew uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const topic = process.env.TOPIC; - console.log(`Checking if repo has topic: ${topic}`); - - const repoTopics = await github.rest.repos.getAllTopics({ + const { data: values } = await github.request('GET /repos/{owner}/{repo}/properties/values', { owner: context.repo.owner, repo: context.repo.repo }); - console.log(`Repo topics: ${repoTopics.data.names}`); - - const hasTopic = repoTopics.data.names.includes(topic); - console.log(`Has topic: ${hasTopic}`); - - core.setOutput('hasTopic', hasTopic); + const profiles = values.find(property => property.property_name === 'workflow_profiles')?.value ?? []; + if (!Array.isArray(profiles) || !profiles.every(profile => typeof profile === 'string')) { + throw new Error('Invalid workflow_profiles custom property'); + } + const enabled = profiles.includes(process.env.WORKFLOW_PROFILE); + core.info(`Workflow profile ${process.env.WORKFLOW_PROFILE}: ${enabled}`); + core.setOutput('enabled', enabled); - name: Download release asset id: download - if: steps.check-label.outputs.hasTopic == 'true' + if: steps.check-profile.outputs.enabled == 'true' uses: robinraju/release-downloader@28fc21f50d76778e7023361aa1f863e717d3d56f # v1.13 with: repository: "${{ github.repository }}" @@ -62,7 +60,7 @@ jobs: - name: Publish Homebrew Formula if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && fromJson(steps.download.outputs.downloaded_files)[0] uses: LizardByte/actions/actions/release_homebrew@master with: diff --git a/.github/workflows/__call-update-pacman-repo.yml b/.github/workflows/__call-update-pacman-repo.yml index 432a67b6..191f7f8d 100644 --- a/.github/workflows/__call-update-pacman-repo.yml +++ b/.github/workflows/__call-update-pacman-repo.yml @@ -28,31 +28,29 @@ jobs: env: PACMAN_REPO: ${{ github.repository_owner }}/pacman-repo steps: - - name: Check if pacman repo - id: check-label + - name: Check workflow profile + id: check-profile env: - TOPIC: pacman-pkg + WORKFLOW_PROFILE: pacman uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const topic = process.env.TOPIC; - console.log(`Checking if repo has topic: ${topic}`); - - const repoTopics = await github.rest.repos.getAllTopics({ + const { data: values } = await github.request('GET /repos/{owner}/{repo}/properties/values', { owner: context.repo.owner, repo: context.repo.repo }); - console.log(`Repo topics: ${repoTopics.data.names}`); - - const hasTopic = repoTopics.data.names.includes(topic); - console.log(`Has topic: ${hasTopic}`); - - core.setOutput('hasTopic', hasTopic); + const profiles = values.find(property => property.property_name === 'workflow_profiles')?.value ?? []; + if (!Array.isArray(profiles) || !profiles.every(profile => typeof profile === 'string')) { + throw new Error('Invalid workflow_profiles custom property'); + } + const enabled = profiles.includes(process.env.WORKFLOW_PROFILE); + core.info(`Workflow profile ${process.env.WORKFLOW_PROFILE}: ${enabled}`); + core.setOutput('enabled', enabled); - name: Check if latest GitHub release id: check-release if: >- - steps.check-label.outputs.hasTopic == 'true' + steps.check-profile.outputs.enabled == 'true' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | @@ -65,7 +63,7 @@ jobs: - name: Checkout pacman-repo if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -74,7 +72,7 @@ jobs: - name: Prep id: prep if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' run: | echo "pkg_name=$(echo ${{ github.event.repository.name }} | tr '[:upper:]' '[:lower:]')" >> "${GITHUB_OUTPUT}" @@ -82,7 +80,7 @@ jobs: - name: Download release asset id: download if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' uses: robinraju/release-downloader@28fc21f50d76778e7023361aa1f863e717d3d56f # v1.13 with: @@ -96,7 +94,7 @@ jobs: - name: Remove pkg.tar.gz if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' && fromJson(steps.download.outputs.downloaded_files)[0] run: rm -f "${{ fromJson(steps.download.outputs.downloaded_files)[0] }}" @@ -104,7 +102,7 @@ jobs: - name: Create/Update Pull Request id: create-pr if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' && fromJson(steps.download.outputs.downloaded_files)[0] uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 @@ -127,7 +125,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GH_TOKEN }} if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && steps.check-release.outputs.isLatestRelease == 'true' && fromJson(steps.download.outputs.downloaded_files)[0] run: | diff --git a/.github/workflows/__call-update-winget-repo.yml b/.github/workflows/__call-update-winget-repo.yml index ea468181..bf4d15b4 100644 --- a/.github/workflows/__call-update-winget-repo.yml +++ b/.github/workflows/__call-update-winget-repo.yml @@ -3,7 +3,7 @@ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. -# To use, add the `winget-pkg` repository label to identify repositories that should trigger this workflow. +# To use, select `winget` in the `workflow_profiles` repository custom property. # Update Winget on release events. @@ -26,30 +26,28 @@ jobs: name: Winget url: ${{ steps.winget.outputs.pr-url }} steps: - - name: Check if Winget repo - id: check-label + - name: Check workflow profile + id: check-profile env: - TOPIC: winget-pkg + WORKFLOW_PROFILE: winget uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | - const topic = process.env.TOPIC; - console.log(`Checking if repo has topic: ${topic}`); - - const repoTopics = await github.rest.repos.getAllTopics({ + const { data: values } = await github.request('GET /repos/{owner}/{repo}/properties/values', { owner: context.repo.owner, repo: context.repo.repo }); - console.log(`Repo topics: ${repoTopics.data.names}`); - - const hasTopic = repoTopics.data.names.includes(topic); - console.log(`Has topic: ${hasTopic}`); - - core.setOutput('hasTopic', hasTopic); + const profiles = values.find(property => property.property_name === 'workflow_profiles')?.value ?? []; + if (!Array.isArray(profiles) || !profiles.every(profile => typeof profile === 'string')) { + throw new Error('Invalid workflow_profiles custom property'); + } + const enabled = profiles.includes(process.env.WORKFLOW_PROFILE); + core.info(`Workflow profile ${process.env.WORKFLOW_PROFILE}: ${enabled}`); + core.setOutput('enabled', enabled); - name: Download release asset id: download - if: steps.check-label.outputs.hasTopic == 'true' + if: steps.check-profile.outputs.enabled == 'true' uses: robinraju/release-downloader@28fc21f50d76778e7023361aa1f863e717d3d56f # v1.13 with: repository: "${{ github.repository }}" @@ -62,7 +60,7 @@ jobs: - name: Sync WinGet fork if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && fromJson(steps.download.outputs.downloaded_files)[0] env: GH_TOKEN: ${{ secrets.GH_TOKEN }} @@ -72,7 +70,7 @@ jobs: - name: Release to WinGet id: winget if: >- - steps.check-label.outputs.hasTopic == 'true' && + steps.check-profile.outputs.enabled == 'true' && fromJson(steps.download.outputs.downloaded_files)[0] uses: vedantmgoyal9/winget-releaser@b7370d4d152efdce803a26cc6ca9eee58cb2ad54 # v2 with: diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index a1a09ae8..956eeb0f 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -45,7 +45,7 @@ jobs: - name: Synchronize shared files id: sync - uses: LizardByte/actions/actions/org_sync@c4828dc659fd8e381db3ace5766cf6470961eb85 # org_sync PR #351 + uses: LizardByte/actions/actions/org_sync@6b31dc36713e84e4f32bca78269d9812ceedf8e6 # org_sync PR #351 with: config: .github/file-sync.json organizations_file: organizations.json diff --git a/.github/workflows/__update-cpm-cmake.yml b/.github/workflows/__update-cpm-cmake.yml index dd949e60..e11dd863 100644 --- a/.github/workflows/__update-cpm-cmake.yml +++ b/.github/workflows/__update-cpm-cmake.yml @@ -1,5 +1,5 @@ --- -# This workflow looks for a CPM.cmake file in each repository in the organization, if the repo has the `cpm` label. +# This workflow looks for CPM.cmake in repos that select `cpm` in the `workflow_profiles` custom property. # If found, it updates the CPM.cmake file to the latest version from the CPM.cmake repository, and # creates a pull request with the changes. @@ -17,6 +17,7 @@ jobs: runs-on: ubuntu-latest outputs: matrix: ${{ steps.set-matrix.outputs.result }} + has-repositories: ${{ steps.set-matrix.outputs.has-repositories }} permissions: {} steps: - name: Get repos @@ -27,21 +28,26 @@ jobs: const opts = github.rest.repos.listForOrg.endpoint.merge({ org: context.repo.owner }); const repos = await github.paginate(opts); - let matrix = { "include": [] }; + const matrix = { "include": [] }; for (const repo of repos) { - const topics = await github.rest.repos.getAllTopics({ + const { data: values } = await github.request('GET /repos/{owner}/{repo}/properties/values', { owner: context.repo.owner, repo: repo.name }); - if (topics.data.names.includes('cpm')) { + const profiles = values.find(property => property.property_name === 'workflow_profiles')?.value ?? []; + if (!Array.isArray(profiles) || !profiles.every(profile => typeof profile === 'string')) { + throw new Error(`Invalid workflow_profiles custom property for ${repo.full_name}`); + } + if (profiles.includes('cpm')) { matrix.include.push({ "repo": repo.name }); } } + core.setOutput('has-repositories', matrix.include.length > 0); return matrix; test-matrix: name: Test Matrix - ${{ matrix.repo }} - if: github.event_name == 'workflow_dispatch' + if: github.event_name == 'workflow_dispatch' && needs.setup-matrix.outputs.has-repositories == 'true' needs: setup-matrix permissions: {} runs-on: ubuntu-latest @@ -54,7 +60,7 @@ jobs: update-cpm-cmake: name: Update CPM.cmake - ${{ matrix.repo }} - if: github.event_name == 'schedule' + if: github.event_name == 'schedule' && needs.setup-matrix.outputs.has-repositories == 'true' needs: setup-matrix permissions: {} runs-on: ubuntu-latest diff --git a/.github/workflows/_ci-node.yml b/.github/workflows/_ci-node.yml index 7faae955..c3c24933 100644 --- a/.github/workflows/_ci-node.yml +++ b/.github/workflows/_ci-node.yml @@ -3,7 +3,7 @@ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. -# To use, add the `nodejs` repository topic to identify repositories that should trigger this workflow. +# To use, select `node_ci` in the `workflow_profiles` repository custom property. # This will run standard CI for Node.js/npm/TypeScript projects. diff --git a/.github/workflows/_update-docs.yml b/.github/workflows/_update-docs.yml index 863f92d5..22c2a517 100644 --- a/.github/workflows/_update-docs.yml +++ b/.github/workflows/_update-docs.yml @@ -3,7 +3,7 @@ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. -# To use, add the `rtd` repository label to identify repositories that should trigger this workflow. +# To use, select `docs` in the `workflow_profiles` repository custom property. # If the project slug is not the repository name, add a repository variable named `READTHEDOCS_SLUG` with the value of # the ReadTheDocs project slug. diff --git a/.github/workflows/_update-flathub-repo.yml b/.github/workflows/_update-flathub-repo.yml index e9d1050d..4b75a248 100644 --- a/.github/workflows/_update-flathub-repo.yml +++ b/.github/workflows/_update-flathub-repo.yml @@ -3,7 +3,7 @@ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. -# To use, add the `flathub-pkg` repository label to identify repositories that should trigger this workflow. +# To use, select `flathub` in the `workflow_profiles` repository custom property. # Update Flathub on release events. diff --git a/.github/workflows/_update-homebrew-repo.yml b/.github/workflows/_update-homebrew-repo.yml index 7b07b4e5..cdd96ddb 100644 --- a/.github/workflows/_update-homebrew-repo.yml +++ b/.github/workflows/_update-homebrew-repo.yml @@ -3,7 +3,7 @@ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. -# To use, add the `homebrew-pkg` repository label to identify repositories that should trigger this workflow. +# To use, select `homebrew` in the `workflow_profiles` repository custom property. # Update Homebrew on release events. diff --git a/.github/workflows/_update-npm.yml b/.github/workflows/_update-npm.yml index df4e2122..a267db10 100644 --- a/.github/workflows/_update-npm.yml +++ b/.github/workflows/_update-npm.yml @@ -3,7 +3,7 @@ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. -# To use, add the `npm-pkg` repository label to identify repositories that should trigger this workflow. +# To use, select `npm` in the `workflow_profiles` repository custom property. # Update NPM packages on release events. diff --git a/.github/workflows/_update-pacman-repo.yml b/.github/workflows/_update-pacman-repo.yml index ca464678..89a67aae 100644 --- a/.github/workflows/_update-pacman-repo.yml +++ b/.github/workflows/_update-pacman-repo.yml @@ -3,7 +3,7 @@ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. -# To use, add the `pacman-pkg` repository label to identify repositories that should trigger this workflow. +# To use, select `pacman` in the `workflow_profiles` repository custom property. # Update pacman repo on release events. diff --git a/.github/workflows/_update-winget-repo.yml b/.github/workflows/_update-winget-repo.yml index d9cf5f17..9bb4d8e9 100644 --- a/.github/workflows/_update-winget-repo.yml +++ b/.github/workflows/_update-winget-repo.yml @@ -3,7 +3,7 @@ # Don't make changes to this file in this repo as they will be overwritten with changes made to the same file in # the above-mentioned repo. -# To use, add the `winget-pkg` repository label to identify repositories that should trigger this workflow. +# To use, select `winget` in the `workflow_profiles` repository custom property. # Update Winget on release events. diff --git a/tests/workflow-profile-selection.test.mjs b/tests/workflow-profile-selection.test.mjs new file mode 100644 index 00000000..424bf00f --- /dev/null +++ b/tests/workflow-profile-selection.test.mjs @@ -0,0 +1,129 @@ +import assert from 'node:assert/strict'; +import {readFileSync, readdirSync} from 'node:fs'; +import test from 'node:test'; + +const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; +const propertyRoute = 'GET /repos/{owner}/{repo}/properties/values'; +const profiles = [ + ['__call-release-notifier.yml', 'release_notifications'], + ['__call-update-flathub-repo.yml', 'flathub'], + ['__call-update-homebrew-repo.yml', 'homebrew'], + ['__call-update-pacman-repo.yml', 'pacman'], + ['__call-update-winget-repo.yml', 'winget'], +]; + +function loadWorkflow(file) { + const source = readFileSync(`.github/workflows/${file}`, 'utf8').replaceAll('\r\n', '\n'); + const lines = source.split(' script: |\n')[1].split('\n'); + const body = []; + for (const line of lines) { + if (line && !line.startsWith(' ')) break; + body.push(line.slice(12)); + } + assert.ok(body.length > 0, `Missing GitHub script: ${file}`); + return {source, execute: new AsyncFunction('github', 'context', 'core', 'process', body.join('\n'))}; +} + +function profileValues(value) { + return value === undefined ? [] : [{property_name: 'workflow_profiles', value}]; +} + +for (const [file, profile] of profiles) { + const {source, execute} = loadWorkflow(file); + async function run(values, error) { + const outputs = {}; + await execute({request: async (route, coordinates) => { + assert.equal(route, propertyRoute); + assert.deepEqual(coordinates, {owner: 'project-org', repo: 'app'}); + if (error) throw error; + return {data: values}; + }}, {repo: {owner: 'project-org', repo: 'app'}}, { + info: () => {}, setOutput: (name, value) => { outputs[name] = value; }, + }, {env: {WORKFLOW_PROFILE: profile}}); + return outputs; + } + + test(`${profile} enables a matching multi-select profile independently of file-sync exclusion`, async () => { + const outputs = await run([...profileValues(['unrelated', profile]), + {property_name: 'sync_mode', value: 'excluded'}]); + assert.deepEqual(outputs, {enabled: true}); + }); + + test(`${profile} skips missing, null, empty, and unrelated profile selections`, async () => { + await Promise.all([undefined, null, [], ['other'], [`${profile}-suffix`]].map(async value => { + assert.deepEqual(await run(profileValues(value)), {enabled: false}); + })); + }); + + test(`${profile} rejects malformed multi-select values and failed API reads`, async () => { + await Promise.all([profile, 1, {}, [profile, null]].map(async value => { + await assert.rejects(run(profileValues(value)), /Invalid workflow_profiles custom property/); + })); + await assert.rejects(run([], new Error('Property access denied')), /Property access denied/); + }); + + test(`${profile} wires the configured profile to every former topic-gated step`, () => { + assert.ok(source.includes(`WORKFLOW_PROFILE: ${profile}`)); + assert.match(source, /id: check-profile/); + assert.match(source, /if:.*(?:\n\s+)?steps.check-profile.outputs.enabled == 'true'/); + assert.doesNotMatch(source, /check-label|hasTopic|getAllTopics|TOPIC:/); + if (['release_notifications', 'flathub', 'pacman'].includes(profile)) { + assert.match(source, /steps.check-release.outputs.isLatestRelease == 'true'/); + } + if (['flathub', 'homebrew', 'pacman', 'winget'].includes(profile)) { + assert.match(source, /fromJson\(steps.download.outputs.downloaded_files\)\[0\]/); + } + }); +} + +const cpm = loadWorkflow('__update-cpm-cmake.yml'); +async function cpmMatrix(repositories, valuesByName, error) { + const outputs = {}; + const github = { + rest: {repos: {listForOrg: {endpoint: {merge: options => options}}}}, + paginate: async options => { + assert.deepEqual(options, {org: 'source-org'}); + return repositories; + }, + request: async (route, {owner, repo}) => { + assert.equal(route, propertyRoute); + assert.equal(owner, 'source-org'); + if (error) throw error; + return {data: profileValues(valuesByName[repo])}; + }, + }; + const matrix = await cpm.execute(github, {repo: {owner: 'source-org'}}, { + setOutput: (name, value) => { outputs[name] = value; }, + }); + return {matrix, outputs}; +} + +test('CPM maintenance selects custom properties instead of public topics', async () => { + const repos = [{name: 'selected', topics: []}, {name: 'topic-only', topics: ['cpm']}, {name: 'unset'}]; + assert.deepEqual(await cpmMatrix(repos, {selected: ['cpm', 'cpp_style'], 'topic-only': ['cpp_style']}), { + matrix: {include: [{repo: 'selected'}]}, outputs: {'has-repositories': true}, + }); +}); + +test('CPM maintenance reports empty selections so both matrix jobs can skip safely', async () => { + assert.deepEqual(await cpmMatrix([{name: 'unset'}, {name: 'null'}], {null: null}), { + matrix: {include: []}, outputs: {'has-repositories': false}, + }); + assert.deepEqual(await cpmMatrix([], {}), { + matrix: {include: []}, outputs: {'has-repositories': false}, + }); + assert.equal(cpm.source.match(/if:.*needs.setup-matrix.outputs.has-repositories == 'true'/g).length, 2); +}); + +test('CPM maintenance fails for malformed selections and inaccessible properties', async () => { + await assert.rejects(cpmMatrix([{name: 'bad', full_name: 'source-org/bad'}], {bad: 'cpm'}), + /Invalid workflow_profiles custom property for source-org\/bad/); + await assert.rejects(cpmMatrix([{name: 'bad'}], {}, new Error('Access denied')), /Access denied/); +}); + +test('workflow automation contains no repository-topic API calls or legacy profile guidance', () => { + for (const file of readdirSync('.github/workflows').filter(file => file.endsWith('.yml'))) { + const source = readFileSync(`.github/workflows/${file}`, 'utf8'); + assert.doesNotMatch(source, /getAllTopics|repository\.topics|repository (?:topic|label)/, file); + } +}); From 75c0f733d029d01ae0b7a4a80a02bec8d8d70368 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:38:03 -0500 Subject: [PATCH 05/22] chore: note next actions release pin update --- .github/workflows/__global-replicator.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index 956eeb0f..310b89e7 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -45,7 +45,8 @@ jobs: - name: Synchronize shared files id: sync - uses: LizardByte/actions/actions/org_sync@6b31dc36713e84e4f32bca78269d9812ceedf8e6 # org_sync PR #351 + # TODO: Update this pin to the next released version of LizardByte/actions. + uses: LizardByte/actions/actions/org_sync@6b31dc36713e84e4f32bca78269d9812ceedf8e6 with: config: .github/file-sync.json organizations_file: organizations.json From 142e39374899d0063edcdfbcec5a2f16a1c8d9e3 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:32:41 -0500 Subject: [PATCH 06/22] ci: synchronize organization configuration and Discord webhooks --- .github/file-sync.md | 40 +- .github/org-sync.json | 491 ++++++++++++++++++++++ .github/org-sync.md | 88 ++++ .github/workflows/__global-replicator.yml | 52 ++- tests/org-sync-policy.test.mjs | 75 ++++ 5 files changed, 726 insertions(+), 20 deletions(-) create mode 100644 .github/org-sync.json create mode 100644 .github/org-sync.md create mode 100644 tests/org-sync-policy.test.mjs diff --git a/.github/file-sync.md b/.github/file-sync.md index 423dbc13..8104d299 100644 --- a/.github/file-sync.md +++ b/.github/file-sync.md @@ -1,19 +1,20 @@ -# File synchronization +# Organization and file synchronization -The **Global replicator** workflow uses `LizardByte/actions/actions/org_sync` to reconcile shared files across -the organizations in [organizations.json](../organizations.json). It replaces the old topic-based replicator +The **Global replicator** workflow uses `LizardByte/actions/actions/org_sync` to reconcile organization configuration +and shared files across the organizations in [organizations.json](../organizations.json). It replaces the old topic-based replicator and the separate preview workflow. A single checkout supplies the policy and source files. -| Event | Mode | Scope | -| ------------------------ | -------- | ------------------------------------------ | -| Internal pull request | Dry run | All configured targets | -| Push to master | Delivery | All configured targets | -| Daily at midnight UTC | Delivery | All configured targets | -| Manual workflow dispatch | Delivery | All targets or the selected org/repository | +| Event | Mode | Scope | +| ------------------------ | -------- | --------------------------------------------- | +| Internal pull request | Dry run | All configured organizations and repositories | +| Push to master | Delivery | All configured organizations and repositories | +| Daily at midnight UTC | Delivery | All configured organizations and repositories | +| Manual workflow dispatch | Delivery | All targets or the selected org/repository | Every pull request and master push runs the workflow regardless of which files changed. Fork pull requests -skip the credentialed job. PR runs never create target Git objects, branches, or pull requests. Delivery runs -create or update one sync PR per repository; they do not merge those PRs. Delivery runs share a concurrency +skip the credentialed job. PR runs do not write organization or target repository state. Delivery runs apply +organization configuration directly, then create or update one sync PR per repository; they do not merge those PRs. +Delivery runs share a concurrency group and are not canceled by subsequent runs. A newer PR preview can cancel an older preview of the same PR. After merging this workflow, use **Run workflow** to optionally select an `organization` or exact @@ -21,6 +22,10 @@ After merging this workflow, use **Run workflow** to optionally select an `organ by the qualified `repository` input. The dedicated bot branch is `bot/org-sync-files`; the action does not take over branches or PRs created by the old replicator. +One job and one action invocation handle both administration and file delivery. An exact `repository` dispatch +is file-only: it omits `properties_file` and `admin_config`, so it cannot change the whole organization while +targeting one repository. Use an `organization` dispatch or an unfiltered run to include administration. + ## Policy and selection - [organizations.json](../organizations.json) is the shared array of five organization names. @@ -28,9 +33,11 @@ take over branches or PRs created by the old replicator. Renovate, use `sync`. `create_if_missing` is available for reviewed exceptions. - [repository-properties.json](../repository-properties.json) records the definitions provisioned in each org. `sync_mode: excluded` skips a repository. Multi-select `workflow_profiles` selects additional file groups. - GitHub does not apply this JSON automatically, and the file-sync action does not read it. Schema definitions - were applied separately through the GitHub API. Updating this file alone does not change live properties; - provisioning/synchronizing schema definitions remains a separate administrative operation. + The action now reads this file and reconciles definitions in every selected org before file delivery. + Repository assignments and unrelated definitions are preserved. Changes to defaults can affect repositories + inheriting them; the PR preview shows those changes before delivery. +- [org-sync.json](org-sync.json) selects shared settings, variables, supplied secrets, blocked users, and security + configurations from LizardByte. See [organization administration](org-sync.md) for the scope and remaining setup. - Public, unarchived repositories are eligible by default, including forks. The source repository is skipped. Existing mirror and Upptime exclusions in LizardByte-infrastructure apply through their properties. - Deselected profiles remove their configured files on the next run. General files remain, and another selected @@ -53,5 +60,6 @@ organizations produce no result rows. The existing `GH_BOT_TOKEN` must read repository metadata/contents and organization custom properties across the listed orgs. Delivery additionally requires permission to write contents/workflows and manage target PRs. The token's user must have access in each organization. Missing access is reported per organization or repository. -The workflow's own `GITHUB_TOKEN` has only `contents: read`. Credentials, properties, secrets, and settings are -provisioned separately from file synchronization. +The workflow's own `GITHUB_TOKEN` has only `contents: read`. Administration uses `ORG_SYNC_ADMIN_TOKEN` when +provided, otherwise `GH_BOT_TOKEN`; that credential needs the organization API permissions described in the +administration guide. Secrets are supplied through explicit environment bindings, never through committed values. diff --git a/.github/org-sync.json b/.github/org-sync.json new file mode 100644 index 00000000..26e4736a --- /dev/null +++ b/.github/org-sync.json @@ -0,0 +1,491 @@ +{ + "version": 1, + "source_organization": "LizardByte", + "organizations": [ + "KokoStreamTV", + "LizardByte-infrastructure", + "SunshineStream", + "Themerr" + ], + "settings": { + "from_source": [ + "company", + "default_repository_permission", + "deploy_keys_enabled_for_repositories", + "has_organization_projects", + "has_repository_projects", + "location", + "members_can_create_internal_repositories", + "members_can_create_pages", + "members_can_create_private_pages", + "members_can_create_private_repositories", + "members_can_create_public_pages", + "members_can_create_public_repositories", + "members_can_create_repositories", + "members_can_fork_private_repositories", + "twitter_username", + "web_commit_signoff_required" + ] + }, + "actions": { + "from_source": [ + "permissions", + "retention", + "workflow" + ] + }, + "blocked_users": { + "from_source": true, + "remove_extra": false + }, + "security_configurations": [ + { + "source_name": "private/internal repositories", + "copy_default": true + }, + { + "source_name": "strict", + "copy_default": true + } + ], + "variables": [ + { + "name": "AUR_SSH_PUBLIC_KEY", + "from_source": "AUR_SSH_PUBLIC_KEY", + "visibility": "all" + }, + { + "name": "AZURE_SIGNING_ACCOUNT", + "from_source": "AZURE_SIGNING_ACCOUNT", + "visibility": "all" + }, + { + "name": "AZURE_SIGNING_CERT_PROFILE", + "from_source": "AZURE_SIGNING_CERT_PROFILE", + "visibility": "all" + }, + { + "name": "AZURE_SIGNING_ENDPOINT", + "from_source": "AZURE_SIGNING_ENDPOINT", + "visibility": "all" + }, + { + "name": "BUFFER_X_CHANNEL_ID", + "from_source": "BUFFER_X_CHANNEL_ID", + "visibility": "selected", + "repository_names": [ + ".github" + ], + "organizations": [ + "LizardByte-infrastructure", + "SunshineStream" + ] + }, + { + "name": "CLA_GIST_ID", + "from_source": "CLA_GIST_ID", + "visibility": "selected", + "repository_names": [ + ".github" + ], + "organizations": [ + "LizardByte-infrastructure", + "SunshineStream" + ] + }, + { + "name": "COLOR_HEX_GREEN", + "from_source": "COLOR_HEX_GREEN", + "visibility": "all" + }, + { + "name": "COLOR_HEX_RED", + "from_source": "COLOR_HEX_RED", + "visibility": "all" + }, + { + "name": "DISCORD_GITHUB_STATUS_CHANNEL_ID", + "from_source": "DISCORD_GITHUB_STATUS_CHANNEL_ID", + "visibility": "all" + }, + { + "name": "DISCORD_REDDIT_CHANNEL_ID", + "from_source": "DISCORD_REDDIT_CHANNEL_ID", + "visibility": "all" + }, + { + "name": "DISCORD_SPONSORS_CHANNEL_ID", + "from_source": "DISCORD_SPONSORS_CHANNEL_ID", + "visibility": "all" + }, + { + "name": "DISCORD_USERNAME", + "from_source": "DISCORD_USERNAME", + "visibility": "all" + }, + { + "name": "DOCKER_HUB_USERNAME", + "from_source": "DOCKER_HUB_USERNAME", + "visibility": "all" + }, + { + "name": "GH_BOT_NAME", + "from_source": "GH_BOT_NAME", + "visibility": "all" + }, + { + "name": "GITLAB_TOKEN_ROTATOR_CLIENT_ID", + "from_source": "GITLAB_TOKEN_ROTATOR_CLIENT_ID", + "visibility": "selected", + "repository_names": [ + ".github" + ], + "organizations": [ + "LizardByte-infrastructure", + "SunshineStream" + ] + }, + { + "name": "ORG_LOGO_URL", + "from_source": "ORG_LOGO_URL", + "visibility": "all" + }, + { + "name": "REDDIT_SUBREDDIT", + "from_source": "REDDIT_SUBREDDIT", + "visibility": "selected", + "repository_names": [ + ".github" + ], + "organizations": [ + "LizardByte-infrastructure", + "SunshineStream" + ] + }, + { + "name": "REDDIT_USERNAME", + "from_source": "REDDIT_USERNAME", + "visibility": "all" + } + ], + "secrets": [ + { + "name": "APPLE_CODESIGN_IDENTITY", + "kind": "actions", + "value_env": "APPLE_CODESIGN_IDENTITY", + "visibility": "all" + }, + { + "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "kind": "actions", + "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "visibility": "all" + }, + { + "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "kind": "actions", + "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "visibility": "all" + }, + { + "name": "APPLE_ID", + "kind": "actions", + "value_env": "APPLE_ID", + "visibility": "all" + }, + { + "name": "APPLE_NOTARYTOOL_PASSWORD", + "kind": "actions", + "value_env": "APPLE_NOTARYTOOL_PASSWORD", + "visibility": "all" + }, + { + "name": "APPLE_TEAM_ID", + "kind": "actions", + "value_env": "APPLE_TEAM_ID", + "visibility": "all" + }, + { + "name": "AUR_SSH_PRIVATE_KEY", + "kind": "actions", + "value_env": "AUR_SSH_PRIVATE_KEY", + "visibility": "all" + }, + { + "name": "AZURE_CLIENT_ID", + "kind": "actions", + "value_env": "AZURE_CLIENT_ID", + "visibility": "all" + }, + { + "name": "AZURE_CLIENT_SECRET", + "kind": "actions", + "value_env": "AZURE_CLIENT_SECRET", + "visibility": "all" + }, + { + "name": "AZURE_TENANT_ID", + "kind": "actions", + "value_env": "AZURE_TENANT_ID", + "visibility": "all" + }, + { + "name": "BUFFER_API_KEY", + "kind": "actions", + "value_env": "BUFFER_API_KEY", + "visibility": "selected", + "repository_names": [ + ".github" + ], + "organizations": [ + "LizardByte-infrastructure", + "SunshineStream" + ] + }, + { + "name": "CLOUDSMITH_API_KEY", + "kind": "actions", + "value_env": "CLOUDSMITH_API_KEY", + "visibility": "selected", + "repository_names": [ + ".github" + ], + "organizations": [ + "LizardByte-infrastructure", + "SunshineStream" + ] + }, + { + "name": "CODECOV_TOKEN", + "kind": "actions", + "value_env": "CODECOV_TOKEN", + "visibility": "all" + }, + { + "name": "COPR_BETA_WEBHOOK_TOKEN", + "kind": "actions", + "value_env": "COPR_BETA_WEBHOOK_TOKEN", + "visibility": "all" + }, + { + "name": "COPR_CLI_CONFIG", + "kind": "actions", + "value_env": "COPR_CLI_CONFIG", + "visibility": "all" + }, + { + "name": "COPR_STABLE_WEBHOOK_TOKEN", + "kind": "actions", + "value_env": "COPR_STABLE_WEBHOOK_TOKEN", + "visibility": "all" + }, + { + "name": "DISCORD_INVITE", + "kind": "actions", + "value_env": "DISCORD_INVITE", + "visibility": "all" + }, + { + "name": "DISCORD_RELEASE_WEBHOOK", + "kind": "actions", + "value_env": "DISCORD_RELEASE_WEBHOOK", + "visibility": "all" + }, + { + "name": "DISCORD_WEBHOOKS", + "kind": "actions", + "value_env": "DISCORD_WEBHOOKS", + "visibility": "all" + }, + { + "name": "DOCKER_HUB_ACCESS_TOKEN", + "kind": "actions", + "value_env": "DOCKER_HUB_ACCESS_TOKEN", + "visibility": "all" + }, + { + "name": "DOCKER_HUB_PASSWORD", + "kind": "actions", + "value_env": "DOCKER_HUB_PASSWORD", + "visibility": "all" + }, + { + "name": "FACEBOOK_ACCESS_TOKEN", + "kind": "actions", + "value_env": "FACEBOOK_ACCESS_TOKEN", + "visibility": "all" + }, + { + "name": "FACEBOOK_PAGE_ID", + "kind": "actions", + "value_env": "FACEBOOK_PAGE_ID", + "visibility": "all" + }, + { + "name": "GH_BOT_EMAIL", + "kind": "actions", + "value_env": "GH_BOT_EMAIL", + "visibility": "all" + }, + { + "name": "GH_BOT_TOKEN", + "kind": "actions", + "value_env": "GH_BOT_TOKEN", + "visibility": "all" + }, + { + "name": "GH_PAT", + "kind": "actions", + "value_env": "GH_PAT", + "visibility": "all" + }, + { + "name": "GITLAB_SSH_PRIVATE_KEY", + "kind": "actions", + "value_env": "GITLAB_SSH_PRIVATE_KEY", + "visibility": "selected", + "repository_names": [ + ".github" + ], + "organizations": [ + "LizardByte-infrastructure", + "SunshineStream" + ] + }, + { + "name": "GITLAB_TOKEN", + "kind": "actions", + "value_env": "GITLAB_TOKEN", + "visibility": "selected", + "repository_names": [ + ".github" + ], + "organizations": [ + "LizardByte-infrastructure", + "SunshineStream" + ] + }, + { + "name": "GITLAB_TOKEN_ROTATOR_PRIVATE_KEY", + "kind": "actions", + "value_env": "GITLAB_TOKEN_ROTATOR_PRIVATE_KEY", + "visibility": "selected", + "repository_names": [ + ".github" + ], + "organizations": [ + "LizardByte-infrastructure", + "SunshineStream" + ] + }, + { + "name": "GRAVATAR_EMAIL", + "kind": "actions", + "value_env": "GRAVATAR_EMAIL", + "visibility": "all" + }, + { + "name": "READTHEDOCS_TOKEN", + "kind": "actions", + "value_env": "READTHEDOCS_TOKEN", + "visibility": "all" + }, + { + "name": "REDDIT_CLIENT_ID", + "kind": "actions", + "value_env": "REDDIT_CLIENT_ID", + "visibility": "all" + }, + { + "name": "REDDIT_CLIENT_SECRET", + "kind": "actions", + "value_env": "REDDIT_CLIENT_SECRET", + "visibility": "all" + }, + { + "name": "REDDIT_FLAIR_ID", + "kind": "actions", + "value_env": "REDDIT_FLAIR_ID", + "visibility": "all" + }, + { + "name": "REDDIT_PASSWORD", + "kind": "actions", + "value_env": "REDDIT_PASSWORD", + "visibility": "all" + }, + { + "name": "VIRUSTOTAL_API_KEY", + "kind": "actions", + "value_env": "VIRUSTOTAL_API_KEY", + "visibility": "all" + }, + { + "name": "CODECOV_TOKEN", + "kind": "dependabot", + "value_env": "CODECOV_TOKEN", + "visibility": "all" + }, + { + "name": "DISCORD_INVITE", + "kind": "dependabot", + "value_env": "DISCORD_INVITE", + "visibility": "all" + }, + { + "name": "GH_BOT_EMAIL", + "kind": "dependabot", + "value_env": "GH_BOT_EMAIL", + "visibility": "all" + }, + { + "name": "GH_BOT_NAME", + "kind": "dependabot", + "value_env": "GH_BOT_NAME", + "visibility": "all" + }, + { + "name": "GH_BOT_TOKEN", + "kind": "dependabot", + "value_env": "GH_BOT_TOKEN", + "visibility": "all" + }, + { + "name": "GRAVATAR_EMAIL", + "kind": "dependabot", + "value_env": "GRAVATAR_EMAIL", + "visibility": "all" + }, + { + "name": "REDDIT_CLIENT_ID", + "kind": "dependabot", + "value_env": "REDDIT_CLIENT_ID", + "visibility": "all" + }, + { + "name": "REDDIT_CLIENT_SECRET", + "kind": "dependabot", + "value_env": "REDDIT_CLIENT_SECRET", + "visibility": "all" + }, + { + "name": "REDDIT_PASSWORD", + "kind": "dependabot", + "value_env": "REDDIT_PASSWORD", + "visibility": "all" + }, + { + "name": "REDDIT_USERNAME", + "kind": "dependabot", + "value_env": "REDDIT_USERNAME", + "visibility": "all" + } + ], + "webhooks": [ + { + "id": "discord", + "source_url_prefix": "https://discord.com/api/webhooks/", + "unsigned": true + } + ] +} diff --git a/.github/org-sync.md b/.github/org-sync.md new file mode 100644 index 00000000..68b99f59 --- /dev/null +++ b/.github/org-sync.md @@ -0,0 +1,88 @@ +# Organization administration + +The Global replicator invokes `org_sync` once for both administration and files. Its +[policy](org-sync.json) uses LizardByte as the source and explicitly authorizes the other four organizations +in [organizations.json](../organizations.json) as destinations. Pull requests preview every operation; +master pushes, the daily schedule, and manual organization/full runs deliver them. Exact-repository runs +replicate files only. Administrative failure prevents file delivery in that invocation. + +## Managed configuration + +| Resource | Policy | +| ------------------------ | ------------------------------------------------------------------------------------------ | +| Custom-property schemas | Reconcile `repository-properties.json` across all five orgs; preserve repository values | +| Shared org settings | Copy 16 shared fields; retain each org's name, description, and website | +| Actions settings | Copy permissions, workflow defaults, and retention | +| Blocked users | Add LizardByte's blocked users; retain destination-only blocks | +| Security configurations | Copy `strict` and `private/internal repositories`, including their new-repository defaults | +| Actions variables | Copy 14 org-wide variables and four selected variables | +| Actions secrets | Supply 31 org-wide secrets and five selected secrets available to this controller | +| Dependabot secrets | Supply ten org-wide secrets from the canonical Actions/variable bindings below | +| Organization webhooks | Copy LizardByte's Discord hooks by URL prefix; exclude CLA Assistant | + +Each project org retains its current name, description, and website. Other shared profile fields, supported +member privileges, default repository permissions, project/Pages creation controls, private forking, signoff, +and deploy-key policy follow LizardByte. A null profile string is treated as empty, matching GitHub's readback. +The source currently allows all Actions, so its inactive selected-actions allowlist is not requested. +If LizardByte switches to selected actions or repositories, update the policy with the corresponding allowlist. + +The two security defaults cover public and private/internal repositories separately. Configuration IDs are +resolved in each destination. Existing repositories are not attached or scanned by this policy. Public/private +feature availability and enterprise licensing still apply. + +## Credentials and access + +Values are supplied through explicit workflow environment bindings. The API cannot export existing secret +plaintext. Org-wide Actions secrets retain `all` visibility. Selected secrets and variables retain `selected` +visibility and are restricted to `.github` in LizardByte-infrastructure and SunshineStream. Their source grants +access to LizardByte/.github; source-only repository names are not copied as destination IDs or added to the +access lists. KokoStreamTV and Themerr do not yet have a `.github` repository, so those selected entries do not +target them. Once those repos exist, add their orgs to the selected entries' destination lists. + +Actions is the canonical supplied value for the eight same-name org-wide Dependabot secrets. The remaining +two Dependabot entries, `GH_BOT_NAME` and `REDDIT_USERNAME`, deliberately use the same-name Actions variables. +This standardizes shared credentials; it does not read or verify the source Dependabot secret plaintext. +Every delivery resends configured secret values because GitHub cannot compare them. Variable writes occur +only when values or access differ. Missing supplied values fail delivery rather than clearing a secret. + +`ORG_SYNC_ADMIN_TOKEN` can provide a separate administration credential; otherwise the action uses +`GH_PAT`, falling back to `GH_BOT_TOKEN`. The credential must administer every destination and read the source. Classic tokens need +`admin:org`; webhook management additionally needs `admin:org_hook`. File delivery still needs repository, +workflow, and pull-request write access. GitHub App tokens need the API-specific organization permissions +listed in the [action guide](https://github.com/LizardByte/actions/blob/codex/org-sync-property-schemas/actions/org_sync/administration.md). +Being an ordinary organization member is insufficient for many administrative operations. + +The explicit destination lists prevent registry additions from distributing credentials automatically. +Schema provisioning still follows the registry. The dedicated `ORG_SYNC_ADMIN_TOKEN` is not in the replication +policy; `GH_PAT` and `GH_BOT_TOKEN` are explicitly shared entries. No secret values or variable values are committed. + +## Discord webhooks + +The action selects `web` hooks under `https://discord.com/api/webhooks/` from the source org at runtime, then +copies each hook's receiver URL, event subscriptions, activity, and content type to the destinations. This +includes the six Discord hooks shown in the source settings and follows future matching hooks automatically. +CLA Assistant is outside this prefix and is not copied. The Discord URL tokens are never committed or written +to the report. Discord receiver authentication is in the URL; `unsigned: true` explicitly configures delivery +without a separate GitHub HMAC signing secret. + +Webhook discovery and replication both happen inside `org_sync`. The source credential must be able to read +those existing hooks. GitHub's OAuth webhook restrictions can produce a filtered empty response despite the +`admin:org_hook` scope; use a suitable PAT/App credential. An empty selected group fails rather than silently +skipping webhook synchronization. PRs only preview the copies; no webhook is created or updated until delivery. + +## Remaining bootstrap work + +- The other 22 selected Actions secrets are not available to LizardByte/.github. Supplying them to the + controller and adding reviewed destination repository mappings is required before they can be synchronized. + They include signing, Crowdin, Codecov API, Discord test, Patreon, Plex, TMDB, Twitch, and YouTube credentials. +- The 18 selected Dependabot secrets need explicit value bindings and repository mappings. Matching secret + names alone do not make their plaintext accessible. Repository mappings can be added after the target repos exist. +- Codespaces organization-secret discovery returned HTTP 404, so no Codespaces secrets are assumed or configured. +- Additional receivers outside the Discord prefix need reviewed `webhooks` entries and, when applicable, + supplied signing-secret bindings. No separate manual webhook replication runs alongside the action. +- Organization default labels/default branch names, extra social links, package policies, standalone-org 2FA, + and PAT allow/approval/lifetime policies require manual setup because their public write APIs are unavailable. + +Review the combined report's organization before/after settings, schema defaults, credential access counts, +and per-operation status before merging. Delivery is not transactional: a failed later write can leave earlier +operations applied. Correct the reported cause and rerun; the action does not roll back newer organization state. diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index 310b89e7..a29d03aa 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -30,7 +30,7 @@ concurrency: jobs: replicate: - name: Replicate files + name: Synchronize organizations and files if: >- github.repository == 'LizardByte/.github' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) @@ -43,14 +43,58 @@ jobs: with: persist-credentials: false - - name: Synchronize shared files + - name: Synchronize organizations and shared files id: sync - # TODO: Update this pin to the next released version of LizardByte/actions. - uses: LizardByte/actions/actions/org_sync@6b31dc36713e84e4f32bca78269d9812ceedf8e6 + # Replace this commit pin with the next released version of LizardByte/actions. + uses: LizardByte/actions/actions/org_sync@a807645c7514ef65af324062998a34ebbe1ff15c + env: + APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }} + APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64: >- + ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64 }} + APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD: >- + ${{ secrets.APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_NOTARYTOOL_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + BUFFER_API_KEY: ${{ secrets.BUFFER_API_KEY }} + CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + COPR_BETA_WEBHOOK_TOKEN: ${{ secrets.COPR_BETA_WEBHOOK_TOKEN }} + COPR_CLI_CONFIG: ${{ secrets.COPR_CLI_CONFIG }} + COPR_STABLE_WEBHOOK_TOKEN: ${{ secrets.COPR_STABLE_WEBHOOK_TOKEN }} + DISCORD_INVITE: ${{ secrets.DISCORD_INVITE }} + DISCORD_RELEASE_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} + DISCORD_WEBHOOKS: ${{ secrets.DISCORD_WEBHOOKS }} + DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} + FACEBOOK_ACCESS_TOKEN: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} + FACEBOOK_PAGE_ID: ${{ secrets.FACEBOOK_PAGE_ID }} + GH_BOT_EMAIL: ${{ secrets.GH_BOT_EMAIL }} + GH_BOT_NAME: ${{ vars.GH_BOT_NAME }} + GH_BOT_TOKEN: ${{ secrets.GH_BOT_TOKEN }} + GH_PAT: ${{ secrets.GH_PAT }} + GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }} + GITLAB_TOKEN_ROTATOR_PRIVATE_KEY: ${{ secrets.GITLAB_TOKEN_ROTATOR_PRIVATE_KEY }} + GRAVATAR_EMAIL: ${{ secrets.GRAVATAR_EMAIL }} + READTHEDOCS_TOKEN: ${{ secrets.READTHEDOCS_TOKEN }} + REDDIT_CLIENT_ID: ${{ secrets.REDDIT_CLIENT_ID }} + REDDIT_CLIENT_SECRET: ${{ secrets.REDDIT_CLIENT_SECRET }} + REDDIT_FLAIR_ID: ${{ secrets.REDDIT_FLAIR_ID }} + REDDIT_PASSWORD: ${{ secrets.REDDIT_PASSWORD }} + REDDIT_USERNAME: ${{ vars.REDDIT_USERNAME }} + VIRUSTOTAL_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }} with: + admin_config: ${{ inputs.repository == '' && '.github/org-sync.json' || '' }} + admin_token: ${{ secrets.ORG_SYNC_ADMIN_TOKEN || secrets.GH_PAT || secrets.GH_BOT_TOKEN }} config: .github/file-sync.json organizations_file: organizations.json organization: ${{ inputs.organization }} repository: ${{ inputs.repository }} + properties_file: ${{ inputs.repository == '' && 'repository-properties.json' || '' }} dry_run: ${{ github.event_name == 'pull_request' }} token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/tests/org-sync-policy.test.mjs b/tests/org-sync-policy.test.mjs new file mode 100644 index 00000000..bdc398d3 --- /dev/null +++ b/tests/org-sync-policy.test.mjs @@ -0,0 +1,75 @@ +import assert from 'node:assert/strict'; +import {readFileSync} from 'node:fs'; +import test from 'node:test'; + +const policy = JSON.parse(readFileSync('.github/org-sync.json', 'utf8')); +const organizations = JSON.parse(readFileSync('organizations.json', 'utf8')); +const workflow = readFileSync('.github/workflows/__global-replicator.yml', 'utf8'); +const compareText = (left, right) => left.localeCompare(right); + +test('the shared policy administers destination orgs while preserving project identities', () => { + assert.equal(policy.source_organization, 'LizardByte'); + assert.deepEqual(policy.organizations, organizations.filter(org => org !== policy.source_organization)); + assert.ok(!policy.organizations.includes(policy.source_organization)); + assert.deepEqual(policy.settings.from_source, policy.settings.from_source.toSorted(compareText)); + for (const field of ['name', 'description', 'blog']) assert.ok(!policy.settings.from_source.includes(field)); + for (const field of ['location', 'twitter_username', 'default_repository_permission', 'members_can_create_repositories']) { + assert.ok(policy.settings.from_source.includes(field)); + } + assert.equal(policy.settings.values, undefined); +}); + +test('active Actions policies, additive blocking, and both security default scopes follow the source', () => { + assert.deepEqual(policy.actions.from_source, ['permissions', 'retention', 'workflow']); + assert.deepEqual(policy.blocked_users, {from_source: true, remove_extra: false}); + assert.deepEqual(policy.security_configurations.map(config => config.source_name), ['private/internal repositories', 'strict']); + assert.ok(policy.security_configurations.every(config => config.copy_default === true)); +}); + +test('Discord hooks are selected at runtime without credential URLs or unrelated integrations', () => { + assert.deepEqual(policy.webhooks, [{id: 'discord', source_url_prefix: 'https://discord.com/api/webhooks/', unsigned: true}]); + assert.doesNotMatch(JSON.stringify(policy), /discord\.com\/api\/webhooks\/[\w-]/); + assert.ok(policy.webhooks.every(entry => entry.url === undefined && entry.source_url === undefined)); + assert.doesNotMatch(JSON.stringify(policy.webhooks), /cla/i); +}); + +test('credential distribution retains explicit destinations, selected access, and unique identities', () => { + const identities = policy.secrets.map(entry => `${entry.kind}/${entry.name}`); + assert.equal(new Set(identities).size, identities.length); + for (const entry of [...policy.variables, ...policy.secrets]) { + assert.ok(['all', 'selected'].includes(entry.visibility)); + assert.ok(!entry.name.startsWith('GITHUB_')); + assert.ok((entry.organizations ?? policy.organizations).every(org => policy.organizations.includes(org))); + if (entry.visibility === 'selected') { + assert.deepEqual(entry.repository_names, ['.github']); + assert.deepEqual(entry.organizations, ['LizardByte-infrastructure', 'SunshineStream']); + } else assert.equal(entry.repository_names, undefined); + } +}); + +test('variables are copied by name and secrets use explicit supplied bindings, with no committed plaintext', () => { + const bindings = workflow.replaceAll(/: >-\r?\n\s+/g, ': '); + for (const entry of policy.variables) { + assert.equal(entry.from_source, entry.name); + assert.equal(entry.value, undefined); + } + for (const entry of policy.secrets) { + assert.equal(entry.value_env, entry.name); + assert.equal(entry.value, undefined); + assert.equal(entry.from_source, undefined); + assert.equal(entry.value_key, undefined); + const context = ['GH_BOT_NAME', 'REDDIT_USERNAME'].includes(entry.name) ? 'vars' : 'secrets'; + assert.ok(bindings.includes(`${entry.value_env}: ` + '${{ ' + `${context}.${entry.name}` + ' }}')); + } + assert.ok(!policy.secrets.some(entry => entry.name === 'ORG_SYNC_ADMIN_TOKEN')); +}); + +test('one action invocation handles schemas and policy while exact-repository dispatch stays file-only', () => { + assert.equal([...workflow.matchAll(/uses: LizardByte\/actions\/actions\/org_sync@/g)].length, 1); + assert.equal([...workflow.slice(workflow.indexOf('\njobs:')).matchAll(/^ [\w-]+:\r?$/gm)].length, 1); + assert.ok(workflow.includes("admin_config: ${{ inputs.repository == '' && '.github/org-sync.json' || '' }}")); + assert.ok(workflow.includes("properties_file: ${{ inputs.repository == '' && 'repository-properties.json' || '' }}")); + assert.ok(workflow.includes('admin_token: ${{ secrets.ORG_SYNC_ADMIN_TOKEN || secrets.GH_PAT || secrets.GH_BOT_TOKEN }}')); + assert.ok(workflow.includes("dry_run: ${{ github.event_name == 'pull_request' }}")); + assert.doesNotMatch(workflow, /toJSON\(secrets\)|toJson\(secrets\)|gh api|hooks\/.*\/pings/); +}); From b0b5b6f381b1c10a71977b8488206ce9ccfe0b6e Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:34:54 -0500 Subject: [PATCH 07/22] docs: pin organization sync reference --- .github/org-sync.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/org-sync.md b/.github/org-sync.md index 68b99f59..ab0961cb 100644 --- a/.github/org-sync.md +++ b/.github/org-sync.md @@ -49,7 +49,7 @@ only when values or access differ. Missing supplied values fail delivery rather `GH_PAT`, falling back to `GH_BOT_TOKEN`. The credential must administer every destination and read the source. Classic tokens need `admin:org`; webhook management additionally needs `admin:org_hook`. File delivery still needs repository, workflow, and pull-request write access. GitHub App tokens need the API-specific organization permissions -listed in the [action guide](https://github.com/LizardByte/actions/blob/codex/org-sync-property-schemas/actions/org_sync/administration.md). +listed in the [action guide](https://github.com/LizardByte/actions/blob/a807645c7514ef65af324062998a34ebbe1ff15c/actions/org_sync/administration.md). Being an ordinary organization member is insufficient for many administrative operations. The explicit destination lists prevent registry additions from distributing credentials automatically. From 7c16982859bcd31f2fcf29e1e927bfa7ab16ff0f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 20:29:25 -0500 Subject: [PATCH 08/22] ci: limit synced credentials to active project consumers --- .github/org-sync.json | 960 +++++++++++++++++----- .github/org-sync.md | 82 +- .github/workflows/__global-replicator.yml | 30 +- tests/fixtures/org-sync-consumers.json | 133 +++ tests/org-sync-policy.test.mjs | 52 +- 5 files changed, 998 insertions(+), 259 deletions(-) create mode 100644 tests/fixtures/org-sync-consumers.json diff --git a/.github/org-sync.json b/.github/org-sync.json index 26e4736a..f47a6a93 100644 --- a/.github/org-sync.json +++ b/.github/org-sync.json @@ -9,7 +9,6 @@ ], "settings": { "from_source": [ - "company", "default_repository_permission", "deploy_keys_enabled_for_repositories", "has_organization_projects", @@ -49,436 +48,987 @@ } ], "variables": [ - { - "name": "AUR_SSH_PUBLIC_KEY", - "from_source": "AUR_SSH_PUBLIC_KEY", - "visibility": "all" - }, { "name": "AZURE_SIGNING_ACCOUNT", "from_source": "AZURE_SIGNING_ACCOUNT", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { - "name": "AZURE_SIGNING_CERT_PROFILE", - "from_source": "AZURE_SIGNING_CERT_PROFILE", - "visibility": "all" + "name": "AZURE_SIGNING_ACCOUNT", + "from_source": "AZURE_SIGNING_ACCOUNT", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { - "name": "AZURE_SIGNING_ENDPOINT", - "from_source": "AZURE_SIGNING_ENDPOINT", - "visibility": "all" + "name": "AZURE_SIGNING_ACCOUNT", + "from_source": "AZURE_SIGNING_ACCOUNT", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "BUFFER_X_CHANNEL_ID", - "from_source": "BUFFER_X_CHANNEL_ID", + "name": "AZURE_SIGNING_CERT_PROFILE", + "from_source": "AZURE_SIGNING_CERT_PROFILE", "visibility": "selected", "repository_names": [ - ".github" + "Koko" ], "organizations": [ - "LizardByte-infrastructure", - "SunshineStream" - ] + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { - "name": "CLA_GIST_ID", - "from_source": "CLA_GIST_ID", + "name": "AZURE_SIGNING_CERT_PROFILE", + "from_source": "AZURE_SIGNING_CERT_PROFILE", "visibility": "selected", "repository_names": [ - ".github" + "Sunshine" ], "organizations": [ - "LizardByte-infrastructure", "SunshineStream" - ] - }, - { - "name": "COLOR_HEX_GREEN", - "from_source": "COLOR_HEX_GREEN", - "visibility": "all" + ], + "missing_repositories": "defer" }, { - "name": "COLOR_HEX_RED", - "from_source": "COLOR_HEX_RED", - "visibility": "all" + "name": "AZURE_SIGNING_CERT_PROFILE", + "from_source": "AZURE_SIGNING_CERT_PROFILE", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "DISCORD_GITHUB_STATUS_CHANNEL_ID", - "from_source": "DISCORD_GITHUB_STATUS_CHANNEL_ID", - "visibility": "all" + "name": "AZURE_SIGNING_ENDPOINT", + "from_source": "AZURE_SIGNING_ENDPOINT", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { - "name": "DISCORD_REDDIT_CHANNEL_ID", - "from_source": "DISCORD_REDDIT_CHANNEL_ID", - "visibility": "all" + "name": "AZURE_SIGNING_ENDPOINT", + "from_source": "AZURE_SIGNING_ENDPOINT", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { - "name": "DISCORD_SPONSORS_CHANNEL_ID", - "from_source": "DISCORD_SPONSORS_CHANNEL_ID", - "visibility": "all" + "name": "AZURE_SIGNING_ENDPOINT", + "from_source": "AZURE_SIGNING_ENDPOINT", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "DISCORD_USERNAME", - "from_source": "DISCORD_USERNAME", - "visibility": "all" + "name": "DOCKER_HUB_USERNAME", + "from_source": "DOCKER_HUB_USERNAME", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { "name": "DOCKER_HUB_USERNAME", "from_source": "DOCKER_HUB_USERNAME", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { "name": "GH_BOT_NAME", "from_source": "GH_BOT_NAME", - "visibility": "all" - }, - { - "name": "GITLAB_TOKEN_ROTATOR_CLIENT_ID", - "from_source": "GITLAB_TOKEN_ROTATOR_CLIENT_ID", "visibility": "selected", "repository_names": [ - ".github" + "Sunshine" ], "organizations": [ - "LizardByte-infrastructure", "SunshineStream" - ] + ], + "missing_repositories": "defer" }, { - "name": "ORG_LOGO_URL", - "from_source": "ORG_LOGO_URL", - "visibility": "all" + "name": "GH_BOT_NAME", + "from_source": "GH_BOT_NAME", + "visibility": "selected", + "repository_names": [ + "Themerr", + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + } + ], + "secrets": [ + { + "name": "APPLE_CODESIGN_IDENTITY", + "kind": "actions", + "value_env": "APPLE_CODESIGN_IDENTITY", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { - "name": "REDDIT_SUBREDDIT", - "from_source": "REDDIT_SUBREDDIT", + "name": "APPLE_CODESIGN_IDENTITY", + "kind": "actions", + "value_env": "APPLE_CODESIGN_IDENTITY", "visibility": "selected", "repository_names": [ - ".github" + "Sunshine" ], "organizations": [ - "LizardByte-infrastructure", "SunshineStream" - ] + ], + "missing_repositories": "defer" }, - { - "name": "REDDIT_USERNAME", - "from_source": "REDDIT_USERNAME", - "visibility": "all" - } - ], - "secrets": [ { "name": "APPLE_CODESIGN_IDENTITY", "kind": "actions", "value_env": "APPLE_CODESIGN_IDENTITY", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", "kind": "actions", "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "kind": "actions", + "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "kind": "actions", + "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", "kind": "actions", "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "kind": "actions", + "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "kind": "actions", + "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_ID", + "kind": "actions", + "value_env": "APPLE_ID", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { "name": "APPLE_ID", "kind": "actions", "value_env": "APPLE_ID", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_NOTARYTOOL_PASSWORD", + "kind": "actions", + "value_env": "APPLE_NOTARYTOOL_PASSWORD", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { "name": "APPLE_NOTARYTOOL_PASSWORD", "kind": "actions", "value_env": "APPLE_NOTARYTOOL_PASSWORD", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { "name": "APPLE_TEAM_ID", "kind": "actions", "value_env": "APPLE_TEAM_ID", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_TEAM_ID", + "kind": "actions", + "value_env": "APPLE_TEAM_ID", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { - "name": "AUR_SSH_PRIVATE_KEY", + "name": "AZURE_CLIENT_ID", "kind": "actions", - "value_env": "AUR_SSH_PRIVATE_KEY", - "visibility": "all" + "value_env": "AZURE_CLIENT_ID", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { "name": "AZURE_CLIENT_ID", "kind": "actions", "value_env": "AZURE_CLIENT_ID", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_CLIENT_ID", + "kind": "actions", + "value_env": "AZURE_CLIENT_ID", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_CLIENT_SECRET", + "kind": "actions", + "value_env": "AZURE_CLIENT_SECRET", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_CLIENT_SECRET", + "kind": "actions", + "value_env": "AZURE_CLIENT_SECRET", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { "name": "AZURE_CLIENT_SECRET", "kind": "actions", "value_env": "AZURE_CLIENT_SECRET", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_TENANT_ID", + "kind": "actions", + "value_env": "AZURE_TENANT_ID", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { "name": "AZURE_TENANT_ID", "kind": "actions", "value_env": "AZURE_TENANT_ID", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_TENANT_ID", + "kind": "actions", + "value_env": "AZURE_TENANT_ID", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "CLOUDSMITH_API_KEY", + "kind": "actions", + "value_env": "CLOUDSMITH_API_KEY", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "CODECOV_TOKEN", + "kind": "actions", + "value_env": "ORG_SYNC_CODECOV_KOKOSTREAMTV", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "CODECOV_TOKEN", + "kind": "actions", + "value_env": "ORG_SYNC_CODECOV_SUNSHINESTREAM", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { - "name": "BUFFER_API_KEY", + "name": "CODECOV_TOKEN", "kind": "actions", - "value_env": "BUFFER_API_KEY", + "value_env": "ORG_SYNC_CODECOV_THEMERR", "visibility": "selected", "repository_names": [ - ".github" + "Themerr", + "ThemerrDB" ], "organizations": [ - "LizardByte-infrastructure", - "SunshineStream" - ] + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "CLOUDSMITH_API_KEY", - "kind": "actions", - "value_env": "CLOUDSMITH_API_KEY", + "name": "CODECOV_TOKEN", + "kind": "dependabot", + "value_env": "ORG_SYNC_CODECOV_KOKOSTREAMTV", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "CODECOV_TOKEN", + "kind": "dependabot", + "value_env": "ORG_SYNC_CODECOV_SUNSHINESTREAM", "visibility": "selected", "repository_names": [ - ".github" + "Sunshine" ], "organizations": [ - "LizardByte-infrastructure", "SunshineStream" - ] + ], + "missing_repositories": "defer" }, { "name": "CODECOV_TOKEN", - "kind": "actions", - "value_env": "CODECOV_TOKEN", - "visibility": "all" + "kind": "dependabot", + "value_env": "ORG_SYNC_CODECOV_THEMERR", + "visibility": "selected", + "repository_names": [ + "Themerr", + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { "name": "COPR_BETA_WEBHOOK_TOKEN", "kind": "actions", "value_env": "COPR_BETA_WEBHOOK_TOKEN", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { "name": "COPR_CLI_CONFIG", "kind": "actions", "value_env": "COPR_CLI_CONFIG", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { "name": "COPR_STABLE_WEBHOOK_TOKEN", "kind": "actions", "value_env": "COPR_STABLE_WEBHOOK_TOKEN", - "visibility": "all" - }, - { - "name": "DISCORD_INVITE", - "kind": "actions", - "value_env": "DISCORD_INVITE", - "visibility": "all" - }, - { - "name": "DISCORD_RELEASE_WEBHOOK", - "kind": "actions", - "value_env": "DISCORD_RELEASE_WEBHOOK", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { - "name": "DISCORD_WEBHOOKS", + "name": "DOCKER_HUB_ACCESS_TOKEN", "kind": "actions", - "value_env": "DISCORD_WEBHOOKS", - "visibility": "all" + "value_env": "DOCKER_HUB_ACCESS_TOKEN", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { "name": "DOCKER_HUB_ACCESS_TOKEN", "kind": "actions", "value_env": "DOCKER_HUB_ACCESS_TOKEN", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { "name": "DOCKER_HUB_PASSWORD", "kind": "actions", "value_env": "DOCKER_HUB_PASSWORD", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { - "name": "FACEBOOK_ACCESS_TOKEN", + "name": "DOCKER_HUB_PASSWORD", "kind": "actions", - "value_env": "FACEBOOK_ACCESS_TOKEN", - "visibility": "all" + "value_env": "DOCKER_HUB_PASSWORD", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "FACEBOOK_PAGE_ID", + "name": "FANART_PROJECT_API_KEY", "kind": "actions", - "value_env": "FACEBOOK_PAGE_ID", - "visibility": "all" + "value_env": "FANART_PROJECT_API_KEY", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { "name": "GH_BOT_EMAIL", "kind": "actions", "value_env": "GH_BOT_EMAIL", - "visibility": "all" + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { - "name": "GH_BOT_TOKEN", + "name": "GH_BOT_EMAIL", "kind": "actions", - "value_env": "GH_BOT_TOKEN", - "visibility": "all" + "value_env": "GH_BOT_EMAIL", + "visibility": "selected", + "repository_names": [ + "Themerr", + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "GH_PAT", - "kind": "actions", - "value_env": "GH_PAT", - "visibility": "all" + "name": "GH_BOT_EMAIL", + "kind": "dependabot", + "value_env": "GH_BOT_EMAIL", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { - "name": "GITLAB_SSH_PRIVATE_KEY", - "kind": "actions", - "value_env": "GITLAB_SSH_PRIVATE_KEY", + "name": "GH_BOT_EMAIL", + "kind": "dependabot", + "value_env": "GH_BOT_EMAIL", "visibility": "selected", "repository_names": [ - ".github" + "ThemerrDB" ], "organizations": [ - "LizardByte-infrastructure", - "SunshineStream" - ] + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "GITLAB_TOKEN", + "name": "GH_BOT_TOKEN", "kind": "actions", - "value_env": "GITLAB_TOKEN", + "value_env": "GH_BOT_TOKEN", "visibility": "selected", "repository_names": [ - ".github" + "Koko" ], "organizations": [ - "LizardByte-infrastructure", - "SunshineStream" - ] + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { - "name": "GITLAB_TOKEN_ROTATOR_PRIVATE_KEY", + "name": "GH_BOT_TOKEN", "kind": "actions", - "value_env": "GITLAB_TOKEN_ROTATOR_PRIVATE_KEY", + "value_env": "GH_BOT_TOKEN", "visibility": "selected", "repository_names": [ - ".github" + "Sunshine" ], "organizations": [ - "LizardByte-infrastructure", "SunshineStream" - ] + ], + "missing_repositories": "defer" }, { - "name": "GRAVATAR_EMAIL", + "name": "GH_BOT_TOKEN", "kind": "actions", - "value_env": "GRAVATAR_EMAIL", - "visibility": "all" + "value_env": "GH_BOT_TOKEN", + "visibility": "selected", + "repository_names": [ + "Themerr", + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "READTHEDOCS_TOKEN", - "kind": "actions", - "value_env": "READTHEDOCS_TOKEN", - "visibility": "all" + "name": "GH_BOT_TOKEN", + "kind": "dependabot", + "value_env": "GH_BOT_TOKEN", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "GH_BOT_TOKEN", + "kind": "dependabot", + "value_env": "GH_BOT_TOKEN", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "REDDIT_CLIENT_ID", + "name": "LRCLIB_APPLICATION_ID", "kind": "actions", - "value_env": "REDDIT_CLIENT_ID", - "visibility": "all" + "value_env": "LRCLIB_APPLICATION_ID", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { - "name": "REDDIT_CLIENT_SECRET", + "name": "READTHEDOCS_TOKEN", "kind": "actions", - "value_env": "REDDIT_CLIENT_SECRET", - "visibility": "all" + "value_env": "READTHEDOCS_TOKEN", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { - "name": "REDDIT_FLAIR_ID", + "name": "READTHEDOCS_TOKEN", "kind": "actions", - "value_env": "REDDIT_FLAIR_ID", - "visibility": "all" + "value_env": "READTHEDOCS_TOKEN", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { - "name": "REDDIT_PASSWORD", + "name": "READTHEDOCS_TOKEN", "kind": "actions", - "value_env": "REDDIT_PASSWORD", - "visibility": "all" + "value_env": "READTHEDOCS_TOKEN", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "VIRUSTOTAL_API_KEY", + "name": "TMDB_API_KEY_V3", "kind": "actions", - "value_env": "VIRUSTOTAL_API_KEY", - "visibility": "all" + "value_env": "TMDB_API_KEY_V3", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "CODECOV_TOKEN", + "name": "TMDB_API_KEY_V3", "kind": "dependabot", - "value_env": "CODECOV_TOKEN", - "visibility": "all" + "value_env": "TMDB_API_KEY_V3", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "DISCORD_INVITE", - "kind": "dependabot", - "value_env": "DISCORD_INVITE", - "visibility": "all" + "name": "TWITCH_CLIENT_ID", + "kind": "actions", + "value_env": "TWITCH_CLIENT_ID", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "GH_BOT_EMAIL", + "name": "TWITCH_CLIENT_ID", "kind": "dependabot", - "value_env": "GH_BOT_EMAIL", - "visibility": "all" + "value_env": "TWITCH_CLIENT_ID", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "GH_BOT_NAME", - "kind": "dependabot", - "value_env": "GH_BOT_NAME", - "visibility": "all" + "name": "TWITCH_CLIENT_SECRET", + "kind": "actions", + "value_env": "TWITCH_CLIENT_SECRET", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "GH_BOT_TOKEN", + "name": "TWITCH_CLIENT_SECRET", "kind": "dependabot", - "value_env": "GH_BOT_TOKEN", - "visibility": "all" + "value_env": "TWITCH_CLIENT_SECRET", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "GRAVATAR_EMAIL", - "kind": "dependabot", - "value_env": "GRAVATAR_EMAIL", - "visibility": "all" + "name": "VIRUSTOTAL_API_KEY", + "kind": "actions", + "value_env": "VIRUSTOTAL_API_KEY", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" }, { - "name": "REDDIT_CLIENT_ID", - "kind": "dependabot", - "value_env": "REDDIT_CLIENT_ID", - "visibility": "all" + "name": "VIRUSTOTAL_API_KEY", + "kind": "actions", + "value_env": "VIRUSTOTAL_API_KEY", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" }, { - "name": "REDDIT_CLIENT_SECRET", - "kind": "dependabot", - "value_env": "REDDIT_CLIENT_SECRET", - "visibility": "all" + "name": "VIRUSTOTAL_API_KEY", + "kind": "actions", + "value_env": "VIRUSTOTAL_API_KEY", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "REDDIT_PASSWORD", - "kind": "dependabot", - "value_env": "REDDIT_PASSWORD", - "visibility": "all" + "name": "YOUTUBE_API_KEY", + "kind": "actions", + "value_env": "YOUTUBE_API_KEY", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" }, { - "name": "REDDIT_USERNAME", + "name": "YOUTUBE_API_KEY", "kind": "dependabot", - "value_env": "REDDIT_USERNAME", - "visibility": "all" + "value_env": "YOUTUBE_API_KEY", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" } ], "webhooks": [ diff --git a/.github/org-sync.md b/.github/org-sync.md index ab0961cb..6714422c 100644 --- a/.github/org-sync.md +++ b/.github/org-sync.md @@ -11,16 +11,17 @@ replicate files only. Administrative failure prevents file delivery in that invo | Resource | Policy | | ------------------------ | ------------------------------------------------------------------------------------------ | | Custom-property schemas | Reconcile `repository-properties.json` across all five orgs; preserve repository values | -| Shared org settings | Copy 16 shared fields; retain each org's name, description, and website | +| Shared org settings | Copy 15 shared fields; retain each org's company, name, description, and website | | Actions settings | Copy permissions, workflow defaults, and retention | | Blocked users | Add LizardByte's blocked users; retain destination-only blocks | | Security configurations | Copy `strict` and `private/internal repositories`, including their new-repository defaults | -| Actions variables | Copy 14 org-wide variables and four selected variables | -| Actions secrets | Supply 31 org-wide secrets and five selected secrets available to this controller | -| Dependabot secrets | Supply ten org-wide secrets from the canonical Actions/variable bindings below | +| Actions variables | Copy five shared variable names only to their audited consumers | +| Actions secrets | Supply 25 shared secret names plus destination-specific Codecov tokens | +| Dependabot secrets | Supply only the audited pull-request consumers below | | Organization webhooks | Copy LizardByte's Discord hooks by URL prefix; exclude CLA Assistant | -Each project org retains its current name, description, and website. Other shared profile fields, supported +Each project org retains its current company, name, description, and website. `company` is a separate profile +field; the organization display name uses `name`. Other shared profile fields, supported member privileges, default repository permissions, project/Pages creation controls, private forking, signoff, and deploy-key policy follow LizardByte. A null profile string is treated as empty, matching GitHub's readback. The source currently allows all Actions, so its inactive selected-actions allowlist is not requested. @@ -32,16 +33,49 @@ feature availability and enterprise licensing still apply. ## Credentials and access -Values are supplied through explicit workflow environment bindings. The API cannot export existing secret -plaintext. Org-wide Actions secrets retain `all` visibility. Selected secrets and variables retain `selected` -visibility and are restricted to `.github` in LizardByte-infrastructure and SunshineStream. Their source grants -access to LizardByte/.github; source-only repository names are not copied as destination IDs or added to the -access lists. KokoStreamTV and Themerr do not yet have a `.github` repository, so those selected entries do not -target them. Once those repos exist, add their orgs to the selected entries' destination lists. +The allowlists come from the four active moving repositories and the shared workflows they call, including +Docker, Homebrew, documentation, Pages, Copr, localization, and screenshot publishing. The +[audit fixture](../tests/fixtures/org-sync-consumers.json) records source commits and exact credential names. +Tests reject missing or extra recipients against that audit. Archived Themerr-jellyfin and Themerr-kodi do +not receive credentials. Neither LizardByte-infrastructure nor destination `.github` repositories are recipients. + +| Consumer | Variables | Actions secrets | Dependabot secrets | +| ------------------------ | --------- | --------------- | ------------------ | +| SunshineStream/Sunshine | 5 | 20 | 3 | +| Themerr/Themerr | 5 | 13 | 1 | +| Themerr/ThemerrDB | 1 | 7 | 7 | +| KokoStreamTV/Koko | 3 | 15 | 1 | + +All entries use `selected` visibility and explicit destination organization/repository names. Same-name +entries may have different scopes or values for disjoint orgs. No credentials are granted org-wide or to the +temporary Pages test repository. `missing_repositories: defer` postpones an entry when none of its named +recipients exist; later runs grant only the named repositories that have arrived. Remaining recipients produce +warnings until transferred. Existing credentials are untouched while an entire entry is deferred. + +The five shared variables are `AZURE_SIGNING_ACCOUNT`, `AZURE_SIGNING_CERT_PROFILE`, `AZURE_SIGNING_ENDPOINT`, +`DOCKER_HUB_USERNAME`, and `GH_BOT_NAME`. Each targets only its consumers. `READTHEDOCS_SLUG` is a project-specific +repository override and is not synchronized from the main org. Source-only social, AUR, CLA, GitLab rotation, +logo/color, and monitoring credentials are excluded from the policy and supplied environment. + +Dependabot receives the same Codecov credential as Actions for each active repo. Sunshine additionally needs +the bot token/email for required Homebrew and Pages workflow inputs. ThemerrDB needs its bot token/email for +Pages plus the four API keys used by its database Pages workflow. Release-only signing/publishing credentials +are not copied into Dependabot. Shared secret values come from explicit Actions secret bindings, never API +plaintext exports or speculative copies of every source-org secret. + +Codecov upload tokens have repository/organization scope. The source org's `CODECOV_TOKEN` is not copied. +Each destination's own token is supplied through a dedicated controller binding and written as `CODECOV_TOKEN` +only for that org's audited recipients, in both the Actions and Dependabot namespaces: + +| Destination org | Controller secret binding | +| --------------- | -------------------------------------- | +| KokoStreamTV | `ORG_SYNC_CODECOV_KOKOSTREAMTV` | +| SunshineStream | `ORG_SYNC_CODECOV_SUNSHINESTREAM` | +| Themerr | `ORG_SYNC_CODECOV_THEMERR` | + +See [Codecov's token scopes](https://docs.codecov.com/docs/codecov-tokens). Configure these controller secrets +with tokens from the corresponding Codecov orgs, rather than reusing LizardByte's global token. -Actions is the canonical supplied value for the eight same-name org-wide Dependabot secrets. The remaining -two Dependabot entries, `GH_BOT_NAME` and `REDDIT_USERNAME`, deliberately use the same-name Actions variables. -This standardizes shared credentials; it does not read or verify the source Dependabot secret plaintext. Every delivery resends configured secret values because GitHub cannot compare them. Variable writes occur only when values or access differ. Missing supplied values fail delivery rather than clearing a secret. @@ -49,12 +83,12 @@ only when values or access differ. Missing supplied values fail delivery rather `GH_PAT`, falling back to `GH_BOT_TOKEN`. The credential must administer every destination and read the source. Classic tokens need `admin:org`; webhook management additionally needs `admin:org_hook`. File delivery still needs repository, workflow, and pull-request write access. GitHub App tokens need the API-specific organization permissions -listed in the [action guide](https://github.com/LizardByte/actions/blob/a807645c7514ef65af324062998a34ebbe1ff15c/actions/org_sync/administration.md). +listed in the [action guide](https://github.com/LizardByte/actions/blob/d4c3996b16bc81b160b02c22958150d1724b6f89/actions/org_sync/administration.md). Being an ordinary organization member is insufficient for many administrative operations. The explicit destination lists prevent registry additions from distributing credentials automatically. -Schema provisioning still follows the registry. The dedicated `ORG_SYNC_ADMIN_TOKEN` is not in the replication -policy; `GH_PAT` and `GH_BOT_TOKEN` are explicitly shared entries. No secret values or variable values are committed. +Schema provisioning still follows the registry. Neither `ORG_SYNC_ADMIN_TOKEN` nor `GH_PAT` is replicated. +`GH_BOT_TOKEN` is supplied only to its audited consumers. No secret values or variable values are committed. ## Discord webhooks @@ -72,11 +106,15 @@ skipping webhook synchronization. PRs only preview the copies; no webhook is cre ## Remaining bootstrap work -- The other 22 selected Actions secrets are not available to LizardByte/.github. Supplying them to the - controller and adding reviewed destination repository mappings is required before they can be synchronized. - They include signing, Crowdin, Codecov API, Discord test, Patreon, Plex, TMDB, Twitch, and YouTube credentials. -- The 18 selected Dependabot secrets need explicit value bindings and repository mappings. Matching secret - names alone do not make their plaintext accessible. Repository mappings can be added after the target repos exist. +- The controller must retain access to six source Actions secrets: `FANART_PROJECT_API_KEY` and + `LRCLIB_APPLICATION_ID` for Koko, plus `TMDB_API_KEY_V3`, `TWITCH_CLIENT_ID`, `TWITCH_CLIENT_SECRET`, and + `YOUTUBE_API_KEY` for ThemerrDB. Controller access was provisioned with approval by adding only LizardByte/.github + to those selected-repository lists and verifying that previous recipients remained. No Plex, Patreon, Crowdin, + social, or unrelated API credentials are needed by this audit. +- Supply the three destination-specific Codecov bindings above before their recipients transfer. Once a + recipient exists, missing supplied credentials fail delivery rather than silently creating an unusable setup. +- Some source workflows still hard-code the LizardByte owner or old publishing locations. Update those as part + of transfer preparation; this policy covers their intended consumers after the owner guards are corrected. - Codespaces organization-secret discovery returned HTTP 404, so no Codespaces secrets are assumed or configured. - Additional receivers outside the Discord prefix need reviewed `webhooks` entries and, when applicable, supplied signing-secret bindings. No separate manual webhook replication runs alongside the action. diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index a29d03aa..fb6cec9c 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -46,7 +46,7 @@ jobs: - name: Synchronize organizations and shared files id: sync # Replace this commit pin with the next released version of LizardByte/actions. - uses: LizardByte/actions/actions/org_sync@a807645c7514ef65af324062998a34ebbe1ff15c + uses: LizardByte/actions/actions/org_sync@d4c3996b16bc81b160b02c22958150d1724b6f89 env: APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }} APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64: >- @@ -56,38 +56,28 @@ jobs: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_NOTARYTOOL_PASSWORD: ${{ secrets.APPLE_NOTARYTOOL_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} - BUFFER_API_KEY: ${{ secrets.BUFFER_API_KEY }} CLOUDSMITH_API_KEY: ${{ secrets.CLOUDSMITH_API_KEY }} - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} COPR_BETA_WEBHOOK_TOKEN: ${{ secrets.COPR_BETA_WEBHOOK_TOKEN }} COPR_CLI_CONFIG: ${{ secrets.COPR_CLI_CONFIG }} COPR_STABLE_WEBHOOK_TOKEN: ${{ secrets.COPR_STABLE_WEBHOOK_TOKEN }} - DISCORD_INVITE: ${{ secrets.DISCORD_INVITE }} - DISCORD_RELEASE_WEBHOOK: ${{ secrets.DISCORD_RELEASE_WEBHOOK }} - DISCORD_WEBHOOKS: ${{ secrets.DISCORD_WEBHOOKS }} DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} - FACEBOOK_ACCESS_TOKEN: ${{ secrets.FACEBOOK_ACCESS_TOKEN }} - FACEBOOK_PAGE_ID: ${{ secrets.FACEBOOK_PAGE_ID }} + FANART_PROJECT_API_KEY: ${{ secrets.FANART_PROJECT_API_KEY }} GH_BOT_EMAIL: ${{ secrets.GH_BOT_EMAIL }} - GH_BOT_NAME: ${{ vars.GH_BOT_NAME }} GH_BOT_TOKEN: ${{ secrets.GH_BOT_TOKEN }} - GH_PAT: ${{ secrets.GH_PAT }} - GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} - GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }} - GITLAB_TOKEN_ROTATOR_PRIVATE_KEY: ${{ secrets.GITLAB_TOKEN_ROTATOR_PRIVATE_KEY }} - GRAVATAR_EMAIL: ${{ secrets.GRAVATAR_EMAIL }} + LRCLIB_APPLICATION_ID: ${{ secrets.LRCLIB_APPLICATION_ID }} + ORG_SYNC_CODECOV_KOKOSTREAMTV: ${{ secrets.ORG_SYNC_CODECOV_KOKOSTREAMTV }} + ORG_SYNC_CODECOV_SUNSHINESTREAM: ${{ secrets.ORG_SYNC_CODECOV_SUNSHINESTREAM }} + ORG_SYNC_CODECOV_THEMERR: ${{ secrets.ORG_SYNC_CODECOV_THEMERR }} READTHEDOCS_TOKEN: ${{ secrets.READTHEDOCS_TOKEN }} - REDDIT_CLIENT_ID: ${{ secrets.REDDIT_CLIENT_ID }} - REDDIT_CLIENT_SECRET: ${{ secrets.REDDIT_CLIENT_SECRET }} - REDDIT_FLAIR_ID: ${{ secrets.REDDIT_FLAIR_ID }} - REDDIT_PASSWORD: ${{ secrets.REDDIT_PASSWORD }} - REDDIT_USERNAME: ${{ vars.REDDIT_USERNAME }} + TMDB_API_KEY_V3: ${{ secrets.TMDB_API_KEY_V3 }} + TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }} + TWITCH_CLIENT_SECRET: ${{ secrets.TWITCH_CLIENT_SECRET }} VIRUSTOTAL_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }} + YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }} with: admin_config: ${{ inputs.repository == '' && '.github/org-sync.json' || '' }} admin_token: ${{ secrets.ORG_SYNC_ADMIN_TOKEN || secrets.GH_PAT || secrets.GH_BOT_TOKEN }} diff --git a/tests/fixtures/org-sync-consumers.json b/tests/fixtures/org-sync-consumers.json new file mode 100644 index 00000000..6da85598 --- /dev/null +++ b/tests/fixtures/org-sync-consumers.json @@ -0,0 +1,133 @@ +{ + "repositories": [ + { + "repository": "SunshineStream/Sunshine", + "source_repository": "LizardByte/Sunshine", + "source_commit": "010f9564e521cb4f3459795187fe87d0719583bf", + "variables": [ + "AZURE_SIGNING_ACCOUNT", + "AZURE_SIGNING_CERT_PROFILE", + "AZURE_SIGNING_ENDPOINT", + "DOCKER_HUB_USERNAME", + "GH_BOT_NAME" + ], + "actions_secrets": [ + "APPLE_CODESIGN_IDENTITY", + "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "APPLE_ID", + "APPLE_NOTARYTOOL_PASSWORD", + "APPLE_TEAM_ID", + "AZURE_CLIENT_ID", + "AZURE_CLIENT_SECRET", + "AZURE_TENANT_ID", + "CLOUDSMITH_API_KEY", + "CODECOV_TOKEN", + "COPR_BETA_WEBHOOK_TOKEN", + "COPR_CLI_CONFIG", + "COPR_STABLE_WEBHOOK_TOKEN", + "DOCKER_HUB_ACCESS_TOKEN", + "DOCKER_HUB_PASSWORD", + "GH_BOT_EMAIL", + "GH_BOT_TOKEN", + "READTHEDOCS_TOKEN", + "VIRUSTOTAL_API_KEY" + ], + "dependabot_secrets": [ + "CODECOV_TOKEN", + "GH_BOT_EMAIL", + "GH_BOT_TOKEN" + ] + }, + { + "repository": "Themerr/Themerr", + "source_repository": "LizardByte/Themerr", + "source_commit": "b1464f27dbc8e5c59740c4e97948a32c183038a3", + "variables": [ + "AZURE_SIGNING_ACCOUNT", + "AZURE_SIGNING_CERT_PROFILE", + "AZURE_SIGNING_ENDPOINT", + "DOCKER_HUB_USERNAME", + "GH_BOT_NAME" + ], + "actions_secrets": [ + "APPLE_CODESIGN_IDENTITY", + "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "AZURE_CLIENT_ID", + "AZURE_CLIENT_SECRET", + "AZURE_TENANT_ID", + "CODECOV_TOKEN", + "DOCKER_HUB_ACCESS_TOKEN", + "DOCKER_HUB_PASSWORD", + "GH_BOT_EMAIL", + "GH_BOT_TOKEN", + "READTHEDOCS_TOKEN", + "VIRUSTOTAL_API_KEY" + ], + "dependabot_secrets": [ + "CODECOV_TOKEN" + ] + }, + { + "repository": "Themerr/ThemerrDB", + "source_repository": "LizardByte/ThemerrDB", + "source_commit": "31aadd8e681992357675da10dfa3e29d9c8615fb", + "variables": [ + "GH_BOT_NAME" + ], + "actions_secrets": [ + "CODECOV_TOKEN", + "GH_BOT_EMAIL", + "GH_BOT_TOKEN", + "TMDB_API_KEY_V3", + "TWITCH_CLIENT_ID", + "TWITCH_CLIENT_SECRET", + "YOUTUBE_API_KEY" + ], + "dependabot_secrets": [ + "CODECOV_TOKEN", + "GH_BOT_EMAIL", + "GH_BOT_TOKEN", + "TMDB_API_KEY_V3", + "TWITCH_CLIENT_ID", + "TWITCH_CLIENT_SECRET", + "YOUTUBE_API_KEY" + ] + }, + { + "repository": "KokoStreamTV/Koko", + "source_repository": "LizardByte/Koko", + "source_commit": "3c32f1f9a0b3cd3f385732e42e3e75194e833cbe", + "variables": [ + "AZURE_SIGNING_ACCOUNT", + "AZURE_SIGNING_CERT_PROFILE", + "AZURE_SIGNING_ENDPOINT" + ], + "actions_secrets": [ + "APPLE_CODESIGN_IDENTITY", + "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "APPLE_ID", + "APPLE_NOTARYTOOL_PASSWORD", + "APPLE_TEAM_ID", + "AZURE_CLIENT_ID", + "AZURE_CLIENT_SECRET", + "AZURE_TENANT_ID", + "CODECOV_TOKEN", + "FANART_PROJECT_API_KEY", + "GH_BOT_TOKEN", + "LRCLIB_APPLICATION_ID", + "READTHEDOCS_TOKEN", + "VIRUSTOTAL_API_KEY" + ], + "dependabot_secrets": [ + "CODECOV_TOKEN" + ] + } + ], + "archived_repositories": [ + "Themerr/Themerr-jellyfin", + "Themerr/Themerr-kodi" + ] +} diff --git a/tests/org-sync-policy.test.mjs b/tests/org-sync-policy.test.mjs index bdc398d3..5f4613e8 100644 --- a/tests/org-sync-policy.test.mjs +++ b/tests/org-sync-policy.test.mjs @@ -5,6 +5,7 @@ import test from 'node:test'; const policy = JSON.parse(readFileSync('.github/org-sync.json', 'utf8')); const organizations = JSON.parse(readFileSync('organizations.json', 'utf8')); const workflow = readFileSync('.github/workflows/__global-replicator.yml', 'utf8'); +const audit = JSON.parse(readFileSync('tests/fixtures/org-sync-consumers.json', 'utf8')); const compareText = (left, right) => left.localeCompare(right); test('the shared policy administers destination orgs while preserving project identities', () => { @@ -12,7 +13,7 @@ test('the shared policy administers destination orgs while preserving project id assert.deepEqual(policy.organizations, organizations.filter(org => org !== policy.source_organization)); assert.ok(!policy.organizations.includes(policy.source_organization)); assert.deepEqual(policy.settings.from_source, policy.settings.from_source.toSorted(compareText)); - for (const field of ['name', 'description', 'blog']) assert.ok(!policy.settings.from_source.includes(field)); + for (const field of ['company', 'name', 'description', 'blog']) assert.ok(!policy.settings.from_source.includes(field)); for (const field of ['location', 'twitter_username', 'default_repository_permission', 'members_can_create_repositories']) { assert.ok(policy.settings.from_source.includes(field)); } @@ -33,17 +34,30 @@ test('Discord hooks are selected at runtime without credential URLs or unrelated assert.doesNotMatch(JSON.stringify(policy.webhooks), /cla/i); }); -test('credential distribution retains explicit destinations, selected access, and unique identities', () => { - const identities = policy.secrets.map(entry => `${entry.kind}/${entry.name}`); +test('credential distribution grants access only to audited active consumers, including future transferred repos', () => { + const identities = policy.secrets.flatMap(entry => entry.organizations.map(org => `${org}/${entry.kind}/${entry.name}`)); assert.equal(new Set(identities).size, identities.length); for (const entry of [...policy.variables, ...policy.secrets]) { - assert.ok(['all', 'selected'].includes(entry.visibility)); + assert.equal(entry.visibility, 'selected'); + assert.equal(entry.missing_repositories, 'defer'); assert.ok(!entry.name.startsWith('GITHUB_')); - assert.ok((entry.organizations ?? policy.organizations).every(org => policy.organizations.includes(org))); - if (entry.visibility === 'selected') { - assert.deepEqual(entry.repository_names, ['.github']); - assert.deepEqual(entry.organizations, ['LizardByte-infrastructure', 'SunshineStream']); - } else assert.equal(entry.repository_names, undefined); + assert.equal(entry.organizations.length, 1); + assert.ok(entry.organizations.every(org => policy.organizations.includes(org))); + assert.ok(entry.repository_names.length > 0); + assert.ok(!entry.repository_names.includes('.github')); + } + const actual = [...policy.variables.map(entry => ({...entry, kind: 'variable'})), ...policy.secrets] + .flatMap(entry => entry.organizations.flatMap(org => entry.repository_names.map(repo => `${org}/${repo}:${entry.kind}:${entry.name}`))); + const expected = audit.repositories.flatMap(repo => [ + ...repo.variables.map(name => `${repo.repository}:variable:${name}`), + ...repo.actions_secrets.map(name => `${repo.repository}:actions:${name}`), + ...repo.dependabot_secrets.map(name => `${repo.repository}:dependabot:${name}`), + ]); + assert.deepEqual(actual.toSorted(compareText), expected.toSorted(compareText)); + for (const repo of audit.archived_repositories) assert.ok(actual.every(value => !value.startsWith(`${repo}:`))); + for (const repo of audit.repositories) { + assert.match(repo.source_commit, /^[a-f\d]{40}$/); + assert.ok(!repo.repository.startsWith('LizardByte-infrastructure/')); } }); @@ -54,16 +68,30 @@ test('variables are copied by name and secrets use explicit supplied bindings, w assert.equal(entry.value, undefined); } for (const entry of policy.secrets) { - assert.equal(entry.value_env, entry.name); + const expectedBinding = entry.name === 'CODECOV_TOKEN' ? `ORG_SYNC_CODECOV_${entry.organizations[0].toUpperCase()}` : entry.name; + assert.equal(entry.value_env, expectedBinding); assert.equal(entry.value, undefined); assert.equal(entry.from_source, undefined); assert.equal(entry.value_key, undefined); - const context = ['GH_BOT_NAME', 'REDDIT_USERNAME'].includes(entry.name) ? 'vars' : 'secrets'; - assert.ok(bindings.includes(`${entry.value_env}: ` + '${{ ' + `${context}.${entry.name}` + ' }}')); + assert.ok(bindings.includes(`${entry.value_env}: ` + '${{ secrets.' + entry.value_env + ' }}')); } assert.ok(!policy.secrets.some(entry => entry.name === 'ORG_SYNC_ADMIN_TOKEN')); }); +test('controller-only, social, and unused credentials are absent from both the policy and its supplied bindings', () => { + const excluded = ['AUR_SSH_PUBLIC_KEY', 'AUR_SSH_PRIVATE_KEY', 'BUFFER_X_CHANNEL_ID', 'BUFFER_API_KEY', 'CLA_GIST_ID', + 'COLOR_HEX_GREEN', 'COLOR_HEX_RED', 'DISCORD_GITHUB_STATUS_CHANNEL_ID', 'DISCORD_REDDIT_CHANNEL_ID', + 'DISCORD_SPONSORS_CHANNEL_ID', 'DISCORD_USERNAME', 'DISCORD_RELEASE_WEBHOOK', 'DISCORD_WEBHOOKS', 'DISCORD_INVITE', + 'FACEBOOK_ACCESS_TOKEN', 'FACEBOOK_PAGE_ID', 'GH_PAT', 'GITLAB_SSH_PRIVATE_KEY', 'GITLAB_TOKEN', + 'GITLAB_TOKEN_ROTATOR_CLIENT_ID', 'GITLAB_TOKEN_ROTATOR_PRIVATE_KEY', 'GRAVATAR_EMAIL', 'ORG_LOGO_URL', + 'REDDIT_CLIENT_ID', 'REDDIT_CLIENT_SECRET', 'REDDIT_PASSWORD', 'REDDIT_USERNAME', 'REDDIT_SUBREDDIT', 'REDDIT_FLAIR_ID']; + for (const name of excluded) { + assert.ok(![...policy.variables, ...policy.secrets].some(entry => entry.name === name)); + assert.doesNotMatch(workflow, new RegExp(`^ ${name}:`, 'm')); + } + assert.doesNotMatch(workflow, /^ CODECOV_TOKEN:/m); +}); + test('one action invocation handles schemas and policy while exact-repository dispatch stays file-only', () => { assert.equal([...workflow.matchAll(/uses: LizardByte\/actions\/actions\/org_sync@/g)].length, 1); assert.equal([...workflow.slice(workflow.indexOf('\njobs:')).matchAll(/^ [\w-]+:\r?$/gm)].length, 1); From 82bae1fa950d9481a41f25d656b3c3e07c6c3a6e Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 20:32:25 -0500 Subject: [PATCH 09/22] ci: use the bot credential for organization administration --- .github/org-sync.md | 3 ++- .github/workflows/__global-replicator.yml | 2 +- tests/org-sync-policy.test.mjs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/org-sync.md b/.github/org-sync.md index 6714422c..413890a4 100644 --- a/.github/org-sync.md +++ b/.github/org-sync.md @@ -80,7 +80,8 @@ Every delivery resends configured secret values because GitHub cannot compare th only when values or access differ. Missing supplied values fail delivery rather than clearing a secret. `ORG_SYNC_ADMIN_TOKEN` can provide a separate administration credential; otherwise the action uses -`GH_PAT`, falling back to `GH_BOT_TOKEN`. The credential must administer every destination and read the source. Classic tokens need +`GH_BOT_TOKEN`. The previous `GH_PAT` fallback returned HTTP 401 and is no longer used. The credential must +administer every destination and read the source. Classic tokens need `admin:org`; webhook management additionally needs `admin:org_hook`. File delivery still needs repository, workflow, and pull-request write access. GitHub App tokens need the API-specific organization permissions listed in the [action guide](https://github.com/LizardByte/actions/blob/d4c3996b16bc81b160b02c22958150d1724b6f89/actions/org_sync/administration.md). diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index fb6cec9c..e15e7d26 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -80,7 +80,7 @@ jobs: YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }} with: admin_config: ${{ inputs.repository == '' && '.github/org-sync.json' || '' }} - admin_token: ${{ secrets.ORG_SYNC_ADMIN_TOKEN || secrets.GH_PAT || secrets.GH_BOT_TOKEN }} + admin_token: ${{ secrets.ORG_SYNC_ADMIN_TOKEN || secrets.GH_BOT_TOKEN }} config: .github/file-sync.json organizations_file: organizations.json organization: ${{ inputs.organization }} diff --git a/tests/org-sync-policy.test.mjs b/tests/org-sync-policy.test.mjs index 5f4613e8..5e79ee36 100644 --- a/tests/org-sync-policy.test.mjs +++ b/tests/org-sync-policy.test.mjs @@ -97,7 +97,7 @@ test('one action invocation handles schemas and policy while exact-repository di assert.equal([...workflow.slice(workflow.indexOf('\njobs:')).matchAll(/^ [\w-]+:\r?$/gm)].length, 1); assert.ok(workflow.includes("admin_config: ${{ inputs.repository == '' && '.github/org-sync.json' || '' }}")); assert.ok(workflow.includes("properties_file: ${{ inputs.repository == '' && 'repository-properties.json' || '' }}")); - assert.ok(workflow.includes('admin_token: ${{ secrets.ORG_SYNC_ADMIN_TOKEN || secrets.GH_PAT || secrets.GH_BOT_TOKEN }}')); + assert.ok(workflow.includes('admin_token: ${{ secrets.ORG_SYNC_ADMIN_TOKEN || secrets.GH_BOT_TOKEN }}')); assert.ok(workflow.includes("dry_run: ${{ github.event_name == 'pull_request' }}")); assert.doesNotMatch(workflow, /toJSON\(secrets\)|toJson\(secrets\)|gh api|hooks\/.*\/pings/); }); From 791648f50911b2f8ceb83b4c19690e70d73fcfdc Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 20:38:48 -0500 Subject: [PATCH 10/22] ci: keep organization administration out of Dependabot runs --- .github/file-sync.md | 2 ++ .github/org-sync.md | 5 +++++ .github/workflows/__global-replicator.yml | 8 ++++++-- tests/org-sync-policy.test.mjs | 7 +++++-- 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/file-sync.md b/.github/file-sync.md index 8104d299..169db823 100644 --- a/.github/file-sync.md +++ b/.github/file-sync.md @@ -25,6 +25,8 @@ take over branches or PRs created by the old replicator. One job and one action invocation handle both administration and file delivery. An exact `repository` dispatch is file-only: it omits `properties_file` and `admin_config`, so it cannot change the whole organization while targeting one repository. Use an `organization` dispatch or an unfiltered run to include administration. +Dependabot PRs still preview files, but omit the administration inputs because GitHub does not provide the +normal Actions admin secret to Dependabot runs. The owner credential is not replicated into Dependabot. ## Policy and selection diff --git a/.github/org-sync.md b/.github/org-sync.md index 413890a4..12f2e4f9 100644 --- a/.github/org-sync.md +++ b/.github/org-sync.md @@ -5,6 +5,8 @@ The Global replicator invokes `org_sync` once for both administration and files. in [organizations.json](../organizations.json) as destinations. Pull requests preview every operation; master pushes, the daily schedule, and manual organization/full runs deliver them. Exact-repository runs replicate files only. Administrative failure prevents file delivery in that invocation. +Dependabot-triggered runs retain file synchronization but omit administration: GitHub withholds normal Actions +secrets from those runs, and the organization-admin credential is not granted to Dependabot. ## Managed configuration @@ -114,6 +116,9 @@ skipping webhook synchronization. PRs only preview the copies; no webhook is cre social, or unrelated API credentials are needed by this audit. - Supply the three destination-specific Codecov bindings above before their recipients transfer. Once a recipient exists, missing supplied credentials fail delivery rather than silently creating an unusable setup. +- Supply `ORG_SYNC_ADMIN_TOKEN` with organization-administration and webhook API access if the bot token lacks + those permissions. The current bot token returned HTTP 403 when reading Actions settings, despite the bot's + verified owner membership. A classic PAT from an owner of all five orgs needs `admin:org` and `admin:org_hook`. - Some source workflows still hard-code the LizardByte owner or old publishing locations. Update those as part of transfer preparation; this policy covers their intended consumers after the owner guards are corrected. - Codespaces organization-secret discovery returned HTTP 404, so no Codespaces secrets are assumed or configured. diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index e15e7d26..3a733f78 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -79,12 +79,16 @@ jobs: VIRUSTOTAL_API_KEY: ${{ secrets.VIRUSTOTAL_API_KEY }} YOUTUBE_API_KEY: ${{ secrets.YOUTUBE_API_KEY }} with: - admin_config: ${{ inputs.repository == '' && '.github/org-sync.json' || '' }} + admin_config: >- + ${{ inputs.repository == '' && github.actor != 'dependabot[bot]' && + '.github/org-sync.json' || '' }} admin_token: ${{ secrets.ORG_SYNC_ADMIN_TOKEN || secrets.GH_BOT_TOKEN }} config: .github/file-sync.json organizations_file: organizations.json organization: ${{ inputs.organization }} repository: ${{ inputs.repository }} - properties_file: ${{ inputs.repository == '' && 'repository-properties.json' || '' }} + properties_file: >- + ${{ inputs.repository == '' && github.actor != 'dependabot[bot]' && + 'repository-properties.json' || '' }} dry_run: ${{ github.event_name == 'pull_request' }} token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/tests/org-sync-policy.test.mjs b/tests/org-sync-policy.test.mjs index 5e79ee36..e857bc7e 100644 --- a/tests/org-sync-policy.test.mjs +++ b/tests/org-sync-policy.test.mjs @@ -95,8 +95,11 @@ test('controller-only, social, and unused credentials are absent from both the p test('one action invocation handles schemas and policy while exact-repository dispatch stays file-only', () => { assert.equal([...workflow.matchAll(/uses: LizardByte\/actions\/actions\/org_sync@/g)].length, 1); assert.equal([...workflow.slice(workflow.indexOf('\njobs:')).matchAll(/^ [\w-]+:\r?$/gm)].length, 1); - assert.ok(workflow.includes("admin_config: ${{ inputs.repository == '' && '.github/org-sync.json' || '' }}")); - assert.ok(workflow.includes("properties_file: ${{ inputs.repository == '' && 'repository-properties.json' || '' }}")); + const folded = workflow.replaceAll(/\s+/g, ' '); + for (const [input, path] of [['admin_config', '.github/org-sync.json'], ['properties_file', 'repository-properties.json']]) { + assert.ok(folded.includes(`${input}: >- ` + "${{ inputs.repository == '' && github.actor != 'dependabot[bot]' && '" + + path + "' || '' }}")); + } assert.ok(workflow.includes('admin_token: ${{ secrets.ORG_SYNC_ADMIN_TOKEN || secrets.GH_BOT_TOKEN }}')); assert.ok(workflow.includes("dry_run: ${{ github.event_name == 'pull_request' }}")); assert.doesNotMatch(workflow, /toJSON\(secrets\)|toJson\(secrets\)|gh api|hooks\/.*\/pings/); From e2d2074139cd84438ccfb46b9732a504a5a34a11 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:08:36 -0500 Subject: [PATCH 11/22] ci: synchronize organization community health defaults --- .github/file-sync.json | 23 +++++++++++++++++++- .github/file-sync.md | 14 ++++++++++--- tests/file-sync-policy.test.mjs | 37 ++++++++++++++++++++++++++------- 3 files changed, 62 insertions(+), 12 deletions(-) diff --git a/.github/file-sync.json b/.github/file-sync.json index 14faeaf4..10af8688 100644 --- a/.github/file-sync.json +++ b/.github/file-sync.json @@ -269,13 +269,34 @@ ] }, { - "id": "organization-pr-template", + "id": "organization-community-health", "repository_names": [ ".github" ], "files": [ + { + "source": ".github/FUNDING.yml" + }, + { + "source": ".github/ISSUE_TEMPLATE/bug-report.yml" + }, + { + "source": ".github/ISSUE_TEMPLATE/config.yml" + }, { "source": ".github/pull_request_template.md" + }, + { + "source": "CODE_OF_CONDUCT.md" + }, + { + "source": "CONTRIBUTING.md" + }, + { + "source": "SECURITY.md" + }, + { + "source": "SUPPORT.md" } ] } diff --git a/.github/file-sync.md b/.github/file-sync.md index 169db823..7cda2a7a 100644 --- a/.github/file-sync.md +++ b/.github/file-sync.md @@ -45,9 +45,17 @@ normal Actions admin secret to Dependabot runs. The owner credential is not repl - Deselected profiles remove their configured files on the next run. General files remain, and another selected rule can retain a shared path. Missing sources fail validation; deletion requires a profile/policy change or explicit cleanup rule. `_docker.yml` is cleanup-only because its source no longer exists. -- PR templates use `repository_names: [".github"]` across `organizations.json`. Adding an org or creating its - `.github` repo needs no policy edit. Organizations without a `.github` repo are skipped for this rule. - Normal eligibility and custom-property checks still apply, including the source-repository exclusion. +- Community health defaults use `repository_names: [".github"]` across `organizations.json`: `FUNDING.yml`, + `CODE_OF_CONDUCT.md`, `CONTRIBUTING.md`, `SECURITY.md`, `SUPPORT.md`, the issue forms/configuration, and the PR template. + These always sync in eligible `.github` repositories, replacing existing versions without requiring a profile. + The `custom_issues` profile still controls issue configuration in ordinary repositories; deselecting it does not + remove an organization's default configuration. Adding an org or creating its `.github` repo needs no policy edit. + Organizations without a `.github` repo are skipped for this rule. Normal eligibility and custom-property checks + still apply, including the source-repository exclusion. + GitHub [inherits these defaults](https://docs.github.com/en/communities/setting-up-your-project-for-healthy-contributions/creating-a-default-community-health-file) + in repositories without their own matching files; repository-specific issue templates/configuration override + the whole default issue-template directory. Other supported defaults such as `ACCESSIBILITY.md`, discussion + forms, and vulnerability-report forms can be added to this rule when source files exist. - The `docker` and `cpm` profile choices currently have no file mappings. CPM maintenance is a separate workflow. ## Reviewing a run diff --git a/tests/file-sync-policy.test.mjs b/tests/file-sync-policy.test.mjs index 11228c3c..7d146094 100644 --- a/tests/file-sync-policy.test.mjs +++ b/tests/file-sync-policy.test.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import {existsSync, readFileSync} from 'node:fs'; +import {existsSync, readFileSync, readdirSync} from 'node:fs'; import test from 'node:test'; const policy = JSON.parse(readFileSync('.github/file-sync.json', 'utf8')); @@ -60,13 +60,34 @@ test('configured sources exist and policy selectors use the shared organization assert.deepEqual(profileSchema.allowed_values, profileSchema.allowed_values.toSorted(compareText)); }); -test('PR templates target .github by name across the shared organization list', () => { - const templates = policy.rules.filter(rule => rule.files.some(file => file.source === '.github/pull_request_template.md')); - assert.equal(templates.length, 1); - assert.deepEqual(templates[0].repository_names, ['.github']); - assert.equal(templates[0].repositories, undefined); - assert.equal(templates[0].organizations, undefined); - assert.equal(templates[0].profile, undefined); +test('all existing community health defaults target .github without a profile or fixed organization list', () => { + const sources = [ + 'ACCESSIBILITY.md', 'CODE_OF_CONDUCT.md', 'CONTRIBUTING.md', 'SECURITY.md', 'SUPPORT.md', + '.github/FUNDING.yml', '.github/VULNERABILITY_REPORT.yml', '.github/VULNERABILITY_REPORT.yaml', + '.github/pull_request_template.md', + ].filter(existsSync); + for (const directory of ['.github/ISSUE_TEMPLATE', '.github/PULL_REQUEST_TEMPLATE', '.github/DISCUSSION_TEMPLATE']) { + if (!existsSync(directory)) continue; + sources.push(...readdirSync(directory, {recursive: true, withFileTypes: true}) + .filter(entry => entry.isFile()) + .map(entry => `${entry.parentPath}/${entry.name}`.replaceAll('\\', '/'))); + } + assert.ok(sources.includes('.github/ISSUE_TEMPLATE/config.yml')); + assert.ok(sources.includes('SUPPORT.md')); + for (const source of sources) { + const defaults = policy.rules.filter(rule => !rule.profile && rule.files.some(file => file.source === source)); + assert.equal(defaults.length, 1, `Missing unconditional community health default: ${source}`); + assert.deepEqual(defaults[0].repository_names, ['.github']); + assert.equal(defaults[0].repositories, undefined); + assert.equal(defaults[0].organizations, undefined); + const file = defaults[0].files.find(file => file.source === source); + assert.equal(file.destination ?? source, source); + assert.equal(file.mode ?? 'sync', 'sync'); + } + const issueConfig = policy.rules.find(rule => rule.profile === 'custom_issues').files + .find(file => file.source === '.github/ISSUE_TEMPLATE/config.yml'); + assert.ok(issueConfig, 'Ordinary repositories must retain profile-controlled issue configuration'); + assert.equal(issueConfig.mode ?? 'sync', 'sync'); assert.equal(policy.include_source, false); assert.equal(policy.include_private, false); assert.equal(policy.include_archived, false); From 0d5351b9b4a29194f0d0a121af8563b72cd6a414 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:11:32 -0500 Subject: [PATCH 12/22] docs: repair support documentation link --- SUPPORT.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/SUPPORT.md b/SUPPORT.md index def71172..0d036bb0 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -8,12 +8,12 @@ **First, read the documentation!** Many common questions are answered in our comprehensive docs. -| Platform | Support Type | Description | -|----------------------------------------------------------------------------|-----------------------|----------------------------------------| -| 📖 [Read the Docs](https://docs.lizardbyte.dev/latest/about/overview.html) | **Documentation** | Official documentation and guides | -| 💬 [Discord](https://discord.com/invite/d6MpcrbYQs) | **Primary Support** | Real-time chat support | -| 🐙 [GitHub Discussions](https://github.com/orgs/LizardByte/discussions) | **Primary Support** | Feature requests and general discourse | -| 👽 [Reddit](https://www.reddit.com/r/LizardByte) | **Community Support** | Community-driven support | +| Platform | Support Type | Description | +|-------------------------------------------------------------------------|-----------------------|----------------------------------------| +| 📖 [Read the Docs](https://docs.lizardbyte.dev/latest/) | **Documentation** | Official documentation and guides | +| 💬 [Discord](https://discord.com/invite/d6MpcrbYQs) | **Primary Support** | Real-time chat support | +| 🐙 [GitHub Discussions](https://github.com/orgs/LizardByte/discussions) | **Primary Support** | Feature requests and general discourse | +| 👽 [Reddit](https://www.reddit.com/r/LizardByte) | **Community Support** | Community-driven support | ## Additional Resources From 89643f64a84e351bc07b7e6a7cadbe4f3a91d185 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:45:43 -0500 Subject: [PATCH 13/22] ci: include awesome-sunshine in organization credential sync --- .github/org-sync.json | 16 ++++++++++++ .github/org-sync.md | 30 +++++++++++++++-------- .github/workflows/__global-replicator.yml | 1 + tests/fixtures/org-sync-consumers.json | 14 +++++++++++ 4 files changed, 51 insertions(+), 10 deletions(-) diff --git a/.github/org-sync.json b/.github/org-sync.json index f47a6a93..0bdd6914 100644 --- a/.github/org-sync.json +++ b/.github/org-sync.json @@ -551,6 +551,7 @@ "value_env": "ORG_SYNC_CODECOV_SUNSHINESTREAM", "visibility": "selected", "repository_names": [ + "awesome-sunshine", "Sunshine" ], "organizations": [ @@ -591,6 +592,7 @@ "value_env": "ORG_SYNC_CODECOV_SUNSHINESTREAM", "visibility": "selected", "repository_names": [ + "awesome-sunshine", "Sunshine" ], "organizations": [ @@ -651,6 +653,19 @@ ], "missing_repositories": "defer" }, + { + "name": "CROWDIN_TOKEN", + "kind": "actions", + "value_env": "CROWDIN_TOKEN", + "visibility": "selected", + "repository_names": [ + "awesome-sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, { "name": "DOCKER_HUB_ACCESS_TOKEN", "kind": "actions", @@ -788,6 +803,7 @@ "value_env": "GH_BOT_TOKEN", "visibility": "selected", "repository_names": [ + "awesome-sunshine", "Sunshine" ], "organizations": [ diff --git a/.github/org-sync.md b/.github/org-sync.md index 12f2e4f9..e869b4ae 100644 --- a/.github/org-sync.md +++ b/.github/org-sync.md @@ -18,7 +18,7 @@ secrets from those runs, and the organization-admin credential is not granted to | Blocked users | Add LizardByte's blocked users; retain destination-only blocks | | Security configurations | Copy `strict` and `private/internal repositories`, including their new-repository defaults | | Actions variables | Copy five shared variable names only to their audited consumers | -| Actions secrets | Supply 25 shared secret names plus destination-specific Codecov tokens | +| Actions secrets | Supply 26 shared secret names plus destination-specific Codecov tokens | | Dependabot secrets | Supply only the audited pull-request consumers below | | Organization webhooks | Copy LizardByte's Discord hooks by URL prefix; exclude CLA Assistant | @@ -35,18 +35,19 @@ feature availability and enterprise licensing still apply. ## Credentials and access -The allowlists come from the four active moving repositories and the shared workflows they call, including +The allowlists come from the five active moving repositories and the shared workflows they call, including Docker, Homebrew, documentation, Pages, Copr, localization, and screenshot publishing. The [audit fixture](../tests/fixtures/org-sync-consumers.json) records source commits and exact credential names. Tests reject missing or extra recipients against that audit. Archived Themerr-jellyfin and Themerr-kodi do not receive credentials. Neither LizardByte-infrastructure nor destination `.github` repositories are recipients. -| Consumer | Variables | Actions secrets | Dependabot secrets | -| ------------------------ | --------- | --------------- | ------------------ | -| SunshineStream/Sunshine | 5 | 20 | 3 | -| Themerr/Themerr | 5 | 13 | 1 | -| Themerr/ThemerrDB | 1 | 7 | 7 | -| KokoStreamTV/Koko | 3 | 15 | 1 | +| Consumer | Variables | Actions secrets | Dependabot secrets | +| ------------------------------- | --------- | --------------- | ------------------ | +| SunshineStream/Sunshine | 5 | 20 | 3 | +| SunshineStream/awesome-sunshine | 0 | 3 | 1 | +| Themerr/Themerr | 5 | 13 | 1 | +| Themerr/ThemerrDB | 1 | 7 | 7 | +| KokoStreamTV/Koko | 3 | 15 | 1 | All entries use `selected` visibility and explicit destination organization/repository names. Same-name entries may have different scopes or values for disjoint orgs. No credentials are granted org-wide or to the @@ -65,6 +66,12 @@ Pages plus the four API keys used by its database Pages workflow. Release-only s are not copied into Dependabot. Shared secret values come from explicit Actions secret bindings, never API plaintext exports or speculative copies of every source-org secret. +`awesome-sunshine` receives only `CROWDIN_TOKEN`, `GH_BOT_TOKEN`, and SunshineStream's own `CODECOV_TOKEN` +in Actions, plus `CODECOV_TOKEN` in Dependabot. Its Crowdin sync runs only for the translation bot or manual +dispatch, and its GitHub bot token is used by the release job, so neither is granted to Dependabot. It has no +shared variable consumers. Update its Crowdin workflow's LizardByte-only owner guard and the shared Node CI +coverage guards before transfer, and retain the `node_ci` workflow profile. + Codecov upload tokens have repository/organization scope. The source org's `CODECOV_TOKEN` is not copied. Each destination's own token is supplied through a dedicated controller binding and written as `CODECOV_TOKEN` only for that org's audited recipients, in both the Actions and Dependabot namespaces: @@ -112,8 +119,11 @@ skipping webhook synchronization. PRs only preview the copies; no webhook is cre - The controller must retain access to six source Actions secrets: `FANART_PROJECT_API_KEY` and `LRCLIB_APPLICATION_ID` for Koko, plus `TMDB_API_KEY_V3`, `TWITCH_CLIENT_ID`, `TWITCH_CLIENT_SECRET`, and `YOUTUBE_API_KEY` for ThemerrDB. Controller access was provisioned with approval by adding only LizardByte/.github - to those selected-repository lists and verifying that previous recipients remained. No Plex, Patreon, Crowdin, - social, or unrelated API credentials are needed by this audit. + to those selected-repository lists and verifying that previous recipients remained. No Plex, Patreon, social, + or unrelated API credentials are needed by this audit. +- Controller access to `CROWDIN_TOKEN` for `awesome-sunshine` was provisioned with approval: LizardByte/.github + was added as the fifth selected source recipient, and all four previous recipients were preserved. Destination + access remains limited to `SunshineStream/awesome-sunshine`. - Supply the three destination-specific Codecov bindings above before their recipients transfer. Once a recipient exists, missing supplied credentials fail delivery rather than silently creating an unusable setup. - Supply `ORG_SYNC_ADMIN_TOKEN` with organization-administration and webhook API access if the bot token lacks diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index 3a733f78..7eb960ad 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -63,6 +63,7 @@ jobs: COPR_BETA_WEBHOOK_TOKEN: ${{ secrets.COPR_BETA_WEBHOOK_TOKEN }} COPR_CLI_CONFIG: ${{ secrets.COPR_CLI_CONFIG }} COPR_STABLE_WEBHOOK_TOKEN: ${{ secrets.COPR_STABLE_WEBHOOK_TOKEN }} + CROWDIN_TOKEN: ${{ secrets.CROWDIN_TOKEN }} DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} DOCKER_HUB_PASSWORD: ${{ secrets.DOCKER_HUB_PASSWORD }} FANART_PROJECT_API_KEY: ${{ secrets.FANART_PROJECT_API_KEY }} diff --git a/tests/fixtures/org-sync-consumers.json b/tests/fixtures/org-sync-consumers.json index 6da85598..d52e645d 100644 --- a/tests/fixtures/org-sync-consumers.json +++ b/tests/fixtures/org-sync-consumers.json @@ -39,6 +39,20 @@ "GH_BOT_TOKEN" ] }, + { + "repository": "SunshineStream/awesome-sunshine", + "source_repository": "LizardByte/awesome-sunshine", + "source_commit": "ef2364ccab13509f273c25e08f6dd380b8f98be5", + "variables": [], + "actions_secrets": [ + "CODECOV_TOKEN", + "CROWDIN_TOKEN", + "GH_BOT_TOKEN" + ], + "dependabot_secrets": [ + "CODECOV_TOKEN" + ] + }, { "repository": "Themerr/Themerr", "source_repository": "LizardByte/Themerr", From 5ba5a48dadcb9f5dda0ea9c681528859bfb7c14f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Fri, 6 Nov 2026 23:51:59 -0500 Subject: [PATCH 14/22] ci: pin org sync to actions v2026.1107.44802 --- .github/workflows/__global-replicator.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index 7eb960ad..52e2b303 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -45,8 +45,7 @@ jobs: - name: Synchronize organizations and shared files id: sync - # Replace this commit pin with the next released version of LizardByte/actions. - uses: LizardByte/actions/actions/org_sync@d4c3996b16bc81b160b02c22958150d1724b6f89 + uses: LizardByte/actions/actions/org_sync@2fedf66b189e56d3f0fc3ba08ec1a0280e027d7e # v2026.1107.44802 env: APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }} APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64: >- From d91bc69f0bc4e077bd2ff6758b41e3a7ae1de34b Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 7 Nov 2026 00:19:43 -0500 Subject: [PATCH 15/22] ci: consolidate synchronization policy and organization discovery --- .github/file-sync.json | 304 --- .github/file-sync.md | 13 +- .github/org-sync.json | 2438 ++++++++++++--------- .github/org-sync.md | 14 +- .github/workflows/__global-replicator.yml | 11 +- repository-properties.json | 41 - tests/file-sync-policy.test.mjs | 7 +- tests/org-sync-policy.test.mjs | 19 +- 8 files changed, 1428 insertions(+), 1419 deletions(-) delete mode 100644 .github/file-sync.json delete mode 100644 repository-properties.json diff --git a/.github/file-sync.json b/.github/file-sync.json deleted file mode 100644 index 10af8688..00000000 --- a/.github/file-sync.json +++ /dev/null @@ -1,304 +0,0 @@ -{ - "version": 1, - "id": "org-sync", - "include_archived": false, - "include_forks": true, - "include_private": false, - "include_source": false, - "rules": [ - { - "id": "obsolete-files", - "files": [ - { - "destination": ".codeql-prebuild-cpp-Linux.sh", - "mode": "remove" - }, - { - "destination": ".codeql-prebuild-cpp-Windows.sh", - "mode": "remove" - }, - { - "destination": ".codeql-prebuild-cpp-macOS.sh", - "mode": "remove" - }, - { - "destination": ".github/label-actions.yml", - "mode": "remove" - }, - { - "destination": ".github/pr_release_template.md", - "mode": "remove" - }, - { - "destination": ".github/workflows/_docker.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/auto-create-pr.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/automerge.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/autoupdate.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/autoupdate-labeler.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/ci-docker.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/ci-qodana.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/codeql.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/common-lint.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/cpp-lint.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/dispatcher.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/issues.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/issues-stale.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/pull-requests.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/python-flake8.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/release-notifier.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/top_issues.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/update-changelog.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/update-docs.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/update-flathub-repo.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/update-homebrew-release.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/update-pacman-repo.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/update-winget-release.yml", - "mode": "remove" - }, - { - "destination": ".github/workflows/yaml-lint.yml", - "mode": "remove" - }, - { - "destination": "codecov.yml", - "mode": "remove" - }, - { - "destination": "qodana-dotnet.yaml", - "mode": "remove" - }, - { - "destination": "qodana-js.yaml", - "mode": "remove" - }, - { - "destination": "qodana-python.yaml", - "mode": "remove" - } - ] - }, - { - "id": "general", - "files": [ - { - "source": ".github/dependabot.yml" - }, - { - "source": ".github/semantic.yml" - }, - { - "source": ".github/workflows/_codeql.yml" - }, - { - "source": ".github/workflows/_common-lint.yml" - }, - { - "source": ".github/workflows/_top-issues.yml" - }, - { - "source": "renovate.json" - } - ] - }, - { - "id": "docs", - "profile": "docs", - "files": [ - { - "source": ".github/workflows/_update-docs.yml" - } - ] - }, - { - "id": "flathub", - "profile": "flathub", - "files": [ - { - "source": ".github/workflows/_update-flathub-repo.yml" - } - ] - }, - { - "id": "homebrew", - "profile": "homebrew", - "files": [ - { - "source": ".github/workflows/_update-homebrew-repo.yml" - } - ] - }, - { - "id": "pacman", - "profile": "pacman", - "files": [ - { - "source": ".github/workflows/_update-pacman-repo.yml" - } - ] - }, - { - "id": "winget", - "profile": "winget", - "files": [ - { - "source": ".github/workflows/_update-winget-repo.yml" - } - ] - }, - { - "id": "node_ci", - "profile": "node_ci", - "files": [ - { - "source": ".github/workflows/_ci-node.yml" - } - ] - }, - { - "id": "npm", - "profile": "npm", - "files": [ - { - "source": ".github/workflows/_update-npm.yml" - } - ] - }, - { - "id": "python_style", - "profile": "python_style", - "files": [ - { - "source": ".flake8" - } - ] - }, - { - "id": "cpp_style", - "profile": "cpp_style", - "files": [ - { - "source": ".clang-format" - } - ] - }, - { - "id": "custom_issues", - "profile": "custom_issues", - "files": [ - { - "source": ".github/ISSUE_TEMPLATE/config.yml" - } - ] - }, - { - "id": "release_notifications", - "profile": "release_notifications", - "files": [ - { - "source": ".github/workflows/_release-notifier.yml" - }, - { - "source": ".github/workflows/_update-changelog.yml" - } - ] - }, - { - "id": "organization-community-health", - "repository_names": [ - ".github" - ], - "files": [ - { - "source": ".github/FUNDING.yml" - }, - { - "source": ".github/ISSUE_TEMPLATE/bug-report.yml" - }, - { - "source": ".github/ISSUE_TEMPLATE/config.yml" - }, - { - "source": ".github/pull_request_template.md" - }, - { - "source": "CODE_OF_CONDUCT.md" - }, - { - "source": "CONTRIBUTING.md" - }, - { - "source": "SECURITY.md" - }, - { - "source": "SUPPORT.md" - } - ] - } - ] -} diff --git a/.github/file-sync.md b/.github/file-sync.md index 7cda2a7a..98331063 100644 --- a/.github/file-sync.md +++ b/.github/file-sync.md @@ -31,15 +31,18 @@ normal Actions admin secret to Dependabot runs. The owner credential is not repl ## Policy and selection - [organizations.json](../organizations.json) is the shared array of five organization names. -- [file-sync.json](file-sync.json) covers the existing file groups and 33 cleanup paths. General files, including - Renovate, use `sync`. `create_if_missing` is available for reviewed exceptions. -- [repository-properties.json](../repository-properties.json) records the definitions provisioned in each org. +- [org-sync.json](org-sync.json) is the single synchronization policy: `files` contains file rules and 33 cleanup + paths, `properties` contains repository property definitions, and `administration` contains shared organization + settings, credentials, blocked users, webhooks, and security configurations. General files, including Renovate, + use `sync`. `create_if_missing` is available for reviewed exceptions. +- The policy's `properties` definitions are provisioned in every registered org. `sync_mode: excluded` skips a repository. Multi-select `workflow_profiles` selects additional file groups. The action now reads this file and reconciles definitions in every selected org before file delivery. Repository assignments and unrelated definitions are preserved. Changes to defaults can affect repositories inheriting them; the PR preview shows those changes before delivery. -- [org-sync.json](org-sync.json) selects shared settings, variables, supplied secrets, blocked users, and security - configurations from LizardByte. See [organization administration](org-sync.md) for the scope and remaining setup. +- Administration targets come directly from `organizations.json`, excluding `source_organization` (LizardByte). + There is no duplicate administration org list. Adding an org enables its shared settings and property schema; + credentials still require explicit recipients. See [organization administration](org-sync.md) for details. - Public, unarchived repositories are eligible by default, including forks. The source repository is skipped. Existing mirror and Upptime exclusions in LizardByte-infrastructure apply through their properties. - Deselected profiles remove their configured files on the next run. General files remain, and another selected diff --git a/.github/org-sync.json b/.github/org-sync.json index 0bdd6914..00156580 100644 --- a/.github/org-sync.json +++ b/.github/org-sync.json @@ -1,1057 +1,1395 @@ { "version": 1, - "source_organization": "LizardByte", - "organizations": [ - "KokoStreamTV", - "LizardByte-infrastructure", - "SunshineStream", - "Themerr" - ], - "settings": { - "from_source": [ - "default_repository_permission", - "deploy_keys_enabled_for_repositories", - "has_organization_projects", - "has_repository_projects", - "location", - "members_can_create_internal_repositories", - "members_can_create_pages", - "members_can_create_private_pages", - "members_can_create_private_repositories", - "members_can_create_public_pages", - "members_can_create_public_repositories", - "members_can_create_repositories", - "members_can_fork_private_repositories", - "twitter_username", - "web_commit_signoff_required" - ] - }, - "actions": { - "from_source": [ - "permissions", - "retention", - "workflow" + "files": { + "id": "org-sync", + "include_archived": false, + "include_forks": true, + "include_private": false, + "include_source": false, + "rules": [ + { + "id": "obsolete-files", + "files": [ + { + "destination": ".codeql-prebuild-cpp-Linux.sh", + "mode": "remove" + }, + { + "destination": ".codeql-prebuild-cpp-Windows.sh", + "mode": "remove" + }, + { + "destination": ".codeql-prebuild-cpp-macOS.sh", + "mode": "remove" + }, + { + "destination": ".github/label-actions.yml", + "mode": "remove" + }, + { + "destination": ".github/pr_release_template.md", + "mode": "remove" + }, + { + "destination": ".github/workflows/_docker.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/auto-create-pr.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/automerge.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/autoupdate.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/autoupdate-labeler.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/ci-docker.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/ci-qodana.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/codeql.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/common-lint.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/cpp-lint.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/dispatcher.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/issues.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/issues-stale.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/pull-requests.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/python-flake8.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/release-notifier.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/top_issues.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/update-changelog.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/update-docs.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/update-flathub-repo.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/update-homebrew-release.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/update-pacman-repo.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/update-winget-release.yml", + "mode": "remove" + }, + { + "destination": ".github/workflows/yaml-lint.yml", + "mode": "remove" + }, + { + "destination": "codecov.yml", + "mode": "remove" + }, + { + "destination": "qodana-dotnet.yaml", + "mode": "remove" + }, + { + "destination": "qodana-js.yaml", + "mode": "remove" + }, + { + "destination": "qodana-python.yaml", + "mode": "remove" + } + ] + }, + { + "id": "general", + "files": [ + { + "source": ".github/dependabot.yml" + }, + { + "source": ".github/semantic.yml" + }, + { + "source": ".github/workflows/_codeql.yml" + }, + { + "source": ".github/workflows/_common-lint.yml" + }, + { + "source": ".github/workflows/_top-issues.yml" + }, + { + "source": "renovate.json" + } + ] + }, + { + "id": "docs", + "profile": "docs", + "files": [ + { + "source": ".github/workflows/_update-docs.yml" + } + ] + }, + { + "id": "flathub", + "profile": "flathub", + "files": [ + { + "source": ".github/workflows/_update-flathub-repo.yml" + } + ] + }, + { + "id": "homebrew", + "profile": "homebrew", + "files": [ + { + "source": ".github/workflows/_update-homebrew-repo.yml" + } + ] + }, + { + "id": "pacman", + "profile": "pacman", + "files": [ + { + "source": ".github/workflows/_update-pacman-repo.yml" + } + ] + }, + { + "id": "winget", + "profile": "winget", + "files": [ + { + "source": ".github/workflows/_update-winget-repo.yml" + } + ] + }, + { + "id": "node_ci", + "profile": "node_ci", + "files": [ + { + "source": ".github/workflows/_ci-node.yml" + } + ] + }, + { + "id": "npm", + "profile": "npm", + "files": [ + { + "source": ".github/workflows/_update-npm.yml" + } + ] + }, + { + "id": "python_style", + "profile": "python_style", + "files": [ + { + "source": ".flake8" + } + ] + }, + { + "id": "cpp_style", + "profile": "cpp_style", + "files": [ + { + "source": ".clang-format" + } + ] + }, + { + "id": "custom_issues", + "profile": "custom_issues", + "files": [ + { + "source": ".github/ISSUE_TEMPLATE/config.yml" + } + ] + }, + { + "id": "release_notifications", + "profile": "release_notifications", + "files": [ + { + "source": ".github/workflows/_release-notifier.yml" + }, + { + "source": ".github/workflows/_update-changelog.yml" + } + ] + }, + { + "id": "organization-community-health", + "repository_names": [ + ".github" + ], + "files": [ + { + "source": ".github/FUNDING.yml" + }, + { + "source": ".github/ISSUE_TEMPLATE/bug-report.yml" + }, + { + "source": ".github/ISSUE_TEMPLATE/config.yml" + }, + { + "source": ".github/pull_request_template.md" + }, + { + "source": "CODE_OF_CONDUCT.md" + }, + { + "source": "CONTRIBUTING.md" + }, + { + "source": "SECURITY.md" + }, + { + "source": "SUPPORT.md" + } + ] + } ] }, - "blocked_users": { - "from_source": true, - "remove_extra": false - }, - "security_configurations": [ - { - "source_name": "private/internal repositories", - "copy_default": true - }, - { - "source_name": "strict", - "copy_default": true - } - ], - "variables": [ - { - "name": "AZURE_SIGNING_ACCOUNT", - "from_source": "AZURE_SIGNING_ACCOUNT", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_SIGNING_ACCOUNT", - "from_source": "AZURE_SIGNING_ACCOUNT", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_SIGNING_ACCOUNT", - "from_source": "AZURE_SIGNING_ACCOUNT", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_SIGNING_CERT_PROFILE", - "from_source": "AZURE_SIGNING_CERT_PROFILE", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_SIGNING_CERT_PROFILE", - "from_source": "AZURE_SIGNING_CERT_PROFILE", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_SIGNING_CERT_PROFILE", - "from_source": "AZURE_SIGNING_CERT_PROFILE", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_SIGNING_ENDPOINT", - "from_source": "AZURE_SIGNING_ENDPOINT", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_SIGNING_ENDPOINT", - "from_source": "AZURE_SIGNING_ENDPOINT", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_SIGNING_ENDPOINT", - "from_source": "AZURE_SIGNING_ENDPOINT", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "DOCKER_HUB_USERNAME", - "from_source": "DOCKER_HUB_USERNAME", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "DOCKER_HUB_USERNAME", - "from_source": "DOCKER_HUB_USERNAME", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "GH_BOT_NAME", - "from_source": "GH_BOT_NAME", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "GH_BOT_NAME", - "from_source": "GH_BOT_NAME", - "visibility": "selected", - "repository_names": [ - "Themerr", - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - } - ], - "secrets": [ - { - "name": "APPLE_CODESIGN_IDENTITY", - "kind": "actions", - "value_env": "APPLE_CODESIGN_IDENTITY", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_CODESIGN_IDENTITY", - "kind": "actions", - "value_env": "APPLE_CODESIGN_IDENTITY", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_CODESIGN_IDENTITY", - "kind": "actions", - "value_env": "APPLE_CODESIGN_IDENTITY", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "kind": "actions", - "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "kind": "actions", - "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "kind": "actions", - "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "kind": "actions", - "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "kind": "actions", - "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "kind": "actions", - "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_ID", - "kind": "actions", - "value_env": "APPLE_ID", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_ID", - "kind": "actions", - "value_env": "APPLE_ID", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_NOTARYTOOL_PASSWORD", - "kind": "actions", - "value_env": "APPLE_NOTARYTOOL_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_NOTARYTOOL_PASSWORD", - "kind": "actions", - "value_env": "APPLE_NOTARYTOOL_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_TEAM_ID", - "kind": "actions", - "value_env": "APPLE_TEAM_ID", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "APPLE_TEAM_ID", - "kind": "actions", - "value_env": "APPLE_TEAM_ID", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_CLIENT_ID", - "kind": "actions", - "value_env": "AZURE_CLIENT_ID", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_CLIENT_ID", - "kind": "actions", - "value_env": "AZURE_CLIENT_ID", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_CLIENT_ID", - "kind": "actions", - "value_env": "AZURE_CLIENT_ID", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_CLIENT_SECRET", - "kind": "actions", - "value_env": "AZURE_CLIENT_SECRET", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_CLIENT_SECRET", - "kind": "actions", - "value_env": "AZURE_CLIENT_SECRET", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_CLIENT_SECRET", - "kind": "actions", - "value_env": "AZURE_CLIENT_SECRET", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_TENANT_ID", - "kind": "actions", - "value_env": "AZURE_TENANT_ID", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_TENANT_ID", - "kind": "actions", - "value_env": "AZURE_TENANT_ID", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "AZURE_TENANT_ID", - "kind": "actions", - "value_env": "AZURE_TENANT_ID", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "CLOUDSMITH_API_KEY", - "kind": "actions", - "value_env": "CLOUDSMITH_API_KEY", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "CODECOV_TOKEN", - "kind": "actions", - "value_env": "ORG_SYNC_CODECOV_KOKOSTREAMTV", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "CODECOV_TOKEN", - "kind": "actions", - "value_env": "ORG_SYNC_CODECOV_SUNSHINESTREAM", - "visibility": "selected", - "repository_names": [ - "awesome-sunshine", - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "CODECOV_TOKEN", - "kind": "actions", - "value_env": "ORG_SYNC_CODECOV_THEMERR", - "visibility": "selected", - "repository_names": [ - "Themerr", - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "CODECOV_TOKEN", - "kind": "dependabot", - "value_env": "ORG_SYNC_CODECOV_KOKOSTREAMTV", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "CODECOV_TOKEN", - "kind": "dependabot", - "value_env": "ORG_SYNC_CODECOV_SUNSHINESTREAM", - "visibility": "selected", - "repository_names": [ - "awesome-sunshine", - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "CODECOV_TOKEN", - "kind": "dependabot", - "value_env": "ORG_SYNC_CODECOV_THEMERR", - "visibility": "selected", - "repository_names": [ - "Themerr", - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "COPR_BETA_WEBHOOK_TOKEN", - "kind": "actions", - "value_env": "COPR_BETA_WEBHOOK_TOKEN", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "COPR_CLI_CONFIG", - "kind": "actions", - "value_env": "COPR_CLI_CONFIG", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "COPR_STABLE_WEBHOOK_TOKEN", - "kind": "actions", - "value_env": "COPR_STABLE_WEBHOOK_TOKEN", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "CROWDIN_TOKEN", - "kind": "actions", - "value_env": "CROWDIN_TOKEN", - "visibility": "selected", - "repository_names": [ - "awesome-sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "DOCKER_HUB_ACCESS_TOKEN", - "kind": "actions", - "value_env": "DOCKER_HUB_ACCESS_TOKEN", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "DOCKER_HUB_ACCESS_TOKEN", - "kind": "actions", - "value_env": "DOCKER_HUB_ACCESS_TOKEN", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "DOCKER_HUB_PASSWORD", - "kind": "actions", - "value_env": "DOCKER_HUB_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "DOCKER_HUB_PASSWORD", - "kind": "actions", - "value_env": "DOCKER_HUB_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "FANART_PROJECT_API_KEY", - "kind": "actions", - "value_env": "FANART_PROJECT_API_KEY", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "GH_BOT_EMAIL", - "kind": "actions", - "value_env": "GH_BOT_EMAIL", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "GH_BOT_EMAIL", - "kind": "actions", - "value_env": "GH_BOT_EMAIL", - "visibility": "selected", - "repository_names": [ - "Themerr", - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "GH_BOT_EMAIL", - "kind": "dependabot", - "value_env": "GH_BOT_EMAIL", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "GH_BOT_EMAIL", - "kind": "dependabot", - "value_env": "GH_BOT_EMAIL", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "GH_BOT_TOKEN", - "kind": "actions", - "value_env": "GH_BOT_TOKEN", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "GH_BOT_TOKEN", - "kind": "actions", - "value_env": "GH_BOT_TOKEN", - "visibility": "selected", - "repository_names": [ - "awesome-sunshine", - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "GH_BOT_TOKEN", - "kind": "actions", - "value_env": "GH_BOT_TOKEN", - "visibility": "selected", - "repository_names": [ - "Themerr", - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "GH_BOT_TOKEN", - "kind": "dependabot", - "value_env": "GH_BOT_TOKEN", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "GH_BOT_TOKEN", - "kind": "dependabot", - "value_env": "GH_BOT_TOKEN", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "LRCLIB_APPLICATION_ID", - "kind": "actions", - "value_env": "LRCLIB_APPLICATION_ID", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "READTHEDOCS_TOKEN", - "kind": "actions", - "value_env": "READTHEDOCS_TOKEN", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "READTHEDOCS_TOKEN", - "kind": "actions", - "value_env": "READTHEDOCS_TOKEN", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "READTHEDOCS_TOKEN", - "kind": "actions", - "value_env": "READTHEDOCS_TOKEN", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "TMDB_API_KEY_V3", - "kind": "actions", - "value_env": "TMDB_API_KEY_V3", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "TMDB_API_KEY_V3", - "kind": "dependabot", - "value_env": "TMDB_API_KEY_V3", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "TWITCH_CLIENT_ID", - "kind": "actions", - "value_env": "TWITCH_CLIENT_ID", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "TWITCH_CLIENT_ID", - "kind": "dependabot", - "value_env": "TWITCH_CLIENT_ID", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "TWITCH_CLIENT_SECRET", - "kind": "actions", - "value_env": "TWITCH_CLIENT_SECRET", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "TWITCH_CLIENT_SECRET", - "kind": "dependabot", - "value_env": "TWITCH_CLIENT_SECRET", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "VIRUSTOTAL_API_KEY", - "kind": "actions", - "value_env": "VIRUSTOTAL_API_KEY", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "VIRUSTOTAL_API_KEY", - "kind": "actions", - "value_env": "VIRUSTOTAL_API_KEY", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "VIRUSTOTAL_API_KEY", - "kind": "actions", - "value_env": "VIRUSTOTAL_API_KEY", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "YOUTUBE_API_KEY", - "kind": "actions", - "value_env": "YOUTUBE_API_KEY", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "YOUTUBE_API_KEY", - "kind": "dependabot", - "value_env": "YOUTUBE_API_KEY", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" + "properties": [ + { + "property_name": "sync_mode", + "value_type": "single_select", + "required": true, + "default_value": "included", + "description": "Shared file synchronization: included by default; excluded opts the repository out.", + "allowed_values": [ + "included", + "excluded" + ], + "values_editable_by": "org_actors", + "require_explicit_values": false + }, + { + "property_name": "workflow_profiles", + "value_type": "multi_select", + "required": false, + "default_value": null, + "description": "Additional shared workflow and configuration groups. No selection means general replication only.", + "allowed_values": [ + "cpm", + "cpp_style", + "custom_issues", + "docker", + "docs", + "flathub", + "homebrew", + "node_ci", + "npm", + "pacman", + "python_style", + "release_notifications", + "winget" + ], + "values_editable_by": "org_actors", + "require_explicit_values": false } ], - "webhooks": [ - { - "id": "discord", - "source_url_prefix": "https://discord.com/api/webhooks/", - "unsigned": true - } - ] + "administration": { + "source_organization": "LizardByte", + "settings": { + "from_source": [ + "default_repository_permission", + "deploy_keys_enabled_for_repositories", + "has_organization_projects", + "has_repository_projects", + "location", + "members_can_create_internal_repositories", + "members_can_create_pages", + "members_can_create_private_pages", + "members_can_create_private_repositories", + "members_can_create_public_pages", + "members_can_create_public_repositories", + "members_can_create_repositories", + "members_can_fork_private_repositories", + "twitter_username", + "web_commit_signoff_required" + ] + }, + "actions": { + "from_source": [ + "permissions", + "retention", + "workflow" + ] + }, + "blocked_users": { + "from_source": true, + "remove_extra": false + }, + "security_configurations": [ + { + "source_name": "private/internal repositories", + "copy_default": true + }, + { + "source_name": "strict", + "copy_default": true + } + ], + "variables": [ + { + "name": "AZURE_SIGNING_ACCOUNT", + "from_source": "AZURE_SIGNING_ACCOUNT", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_SIGNING_ACCOUNT", + "from_source": "AZURE_SIGNING_ACCOUNT", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_SIGNING_ACCOUNT", + "from_source": "AZURE_SIGNING_ACCOUNT", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_SIGNING_CERT_PROFILE", + "from_source": "AZURE_SIGNING_CERT_PROFILE", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_SIGNING_CERT_PROFILE", + "from_source": "AZURE_SIGNING_CERT_PROFILE", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_SIGNING_CERT_PROFILE", + "from_source": "AZURE_SIGNING_CERT_PROFILE", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_SIGNING_ENDPOINT", + "from_source": "AZURE_SIGNING_ENDPOINT", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_SIGNING_ENDPOINT", + "from_source": "AZURE_SIGNING_ENDPOINT", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_SIGNING_ENDPOINT", + "from_source": "AZURE_SIGNING_ENDPOINT", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "DOCKER_HUB_USERNAME", + "from_source": "DOCKER_HUB_USERNAME", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "DOCKER_HUB_USERNAME", + "from_source": "DOCKER_HUB_USERNAME", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "GH_BOT_NAME", + "from_source": "GH_BOT_NAME", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "GH_BOT_NAME", + "from_source": "GH_BOT_NAME", + "visibility": "selected", + "repository_names": [ + "Themerr", + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + } + ], + "secrets": [ + { + "name": "APPLE_CODESIGN_IDENTITY", + "kind": "actions", + "value_env": "APPLE_CODESIGN_IDENTITY", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_CODESIGN_IDENTITY", + "kind": "actions", + "value_env": "APPLE_CODESIGN_IDENTITY", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_CODESIGN_IDENTITY", + "kind": "actions", + "value_env": "APPLE_CODESIGN_IDENTITY", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "kind": "actions", + "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "kind": "actions", + "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "kind": "actions", + "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "kind": "actions", + "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "kind": "actions", + "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "kind": "actions", + "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_ID", + "kind": "actions", + "value_env": "APPLE_ID", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_ID", + "kind": "actions", + "value_env": "APPLE_ID", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_NOTARYTOOL_PASSWORD", + "kind": "actions", + "value_env": "APPLE_NOTARYTOOL_PASSWORD", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_NOTARYTOOL_PASSWORD", + "kind": "actions", + "value_env": "APPLE_NOTARYTOOL_PASSWORD", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_TEAM_ID", + "kind": "actions", + "value_env": "APPLE_TEAM_ID", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "APPLE_TEAM_ID", + "kind": "actions", + "value_env": "APPLE_TEAM_ID", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_CLIENT_ID", + "kind": "actions", + "value_env": "AZURE_CLIENT_ID", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_CLIENT_ID", + "kind": "actions", + "value_env": "AZURE_CLIENT_ID", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_CLIENT_ID", + "kind": "actions", + "value_env": "AZURE_CLIENT_ID", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_CLIENT_SECRET", + "kind": "actions", + "value_env": "AZURE_CLIENT_SECRET", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_CLIENT_SECRET", + "kind": "actions", + "value_env": "AZURE_CLIENT_SECRET", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_CLIENT_SECRET", + "kind": "actions", + "value_env": "AZURE_CLIENT_SECRET", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_TENANT_ID", + "kind": "actions", + "value_env": "AZURE_TENANT_ID", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_TENANT_ID", + "kind": "actions", + "value_env": "AZURE_TENANT_ID", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "AZURE_TENANT_ID", + "kind": "actions", + "value_env": "AZURE_TENANT_ID", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "CLOUDSMITH_API_KEY", + "kind": "actions", + "value_env": "CLOUDSMITH_API_KEY", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "CODECOV_TOKEN", + "kind": "actions", + "value_env": "ORG_SYNC_CODECOV_KOKOSTREAMTV", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "CODECOV_TOKEN", + "kind": "actions", + "value_env": "ORG_SYNC_CODECOV_SUNSHINESTREAM", + "visibility": "selected", + "repository_names": [ + "awesome-sunshine", + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "CODECOV_TOKEN", + "kind": "actions", + "value_env": "ORG_SYNC_CODECOV_THEMERR", + "visibility": "selected", + "repository_names": [ + "Themerr", + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "CODECOV_TOKEN", + "kind": "dependabot", + "value_env": "ORG_SYNC_CODECOV_KOKOSTREAMTV", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "CODECOV_TOKEN", + "kind": "dependabot", + "value_env": "ORG_SYNC_CODECOV_SUNSHINESTREAM", + "visibility": "selected", + "repository_names": [ + "awesome-sunshine", + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "CODECOV_TOKEN", + "kind": "dependabot", + "value_env": "ORG_SYNC_CODECOV_THEMERR", + "visibility": "selected", + "repository_names": [ + "Themerr", + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "COPR_BETA_WEBHOOK_TOKEN", + "kind": "actions", + "value_env": "COPR_BETA_WEBHOOK_TOKEN", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "COPR_CLI_CONFIG", + "kind": "actions", + "value_env": "COPR_CLI_CONFIG", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "COPR_STABLE_WEBHOOK_TOKEN", + "kind": "actions", + "value_env": "COPR_STABLE_WEBHOOK_TOKEN", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "CROWDIN_TOKEN", + "kind": "actions", + "value_env": "CROWDIN_TOKEN", + "visibility": "selected", + "repository_names": [ + "awesome-sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "DOCKER_HUB_ACCESS_TOKEN", + "kind": "actions", + "value_env": "DOCKER_HUB_ACCESS_TOKEN", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "DOCKER_HUB_ACCESS_TOKEN", + "kind": "actions", + "value_env": "DOCKER_HUB_ACCESS_TOKEN", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "DOCKER_HUB_PASSWORD", + "kind": "actions", + "value_env": "DOCKER_HUB_PASSWORD", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "DOCKER_HUB_PASSWORD", + "kind": "actions", + "value_env": "DOCKER_HUB_PASSWORD", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "FANART_PROJECT_API_KEY", + "kind": "actions", + "value_env": "FANART_PROJECT_API_KEY", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "GH_BOT_EMAIL", + "kind": "actions", + "value_env": "GH_BOT_EMAIL", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "GH_BOT_EMAIL", + "kind": "actions", + "value_env": "GH_BOT_EMAIL", + "visibility": "selected", + "repository_names": [ + "Themerr", + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "GH_BOT_EMAIL", + "kind": "dependabot", + "value_env": "GH_BOT_EMAIL", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "GH_BOT_EMAIL", + "kind": "dependabot", + "value_env": "GH_BOT_EMAIL", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "GH_BOT_TOKEN", + "kind": "actions", + "value_env": "GH_BOT_TOKEN", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "GH_BOT_TOKEN", + "kind": "actions", + "value_env": "GH_BOT_TOKEN", + "visibility": "selected", + "repository_names": [ + "awesome-sunshine", + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "GH_BOT_TOKEN", + "kind": "actions", + "value_env": "GH_BOT_TOKEN", + "visibility": "selected", + "repository_names": [ + "Themerr", + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "GH_BOT_TOKEN", + "kind": "dependabot", + "value_env": "GH_BOT_TOKEN", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "GH_BOT_TOKEN", + "kind": "dependabot", + "value_env": "GH_BOT_TOKEN", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "LRCLIB_APPLICATION_ID", + "kind": "actions", + "value_env": "LRCLIB_APPLICATION_ID", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "READTHEDOCS_TOKEN", + "kind": "actions", + "value_env": "READTHEDOCS_TOKEN", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "READTHEDOCS_TOKEN", + "kind": "actions", + "value_env": "READTHEDOCS_TOKEN", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "READTHEDOCS_TOKEN", + "kind": "actions", + "value_env": "READTHEDOCS_TOKEN", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "TMDB_API_KEY_V3", + "kind": "actions", + "value_env": "TMDB_API_KEY_V3", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "TMDB_API_KEY_V3", + "kind": "dependabot", + "value_env": "TMDB_API_KEY_V3", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "TWITCH_CLIENT_ID", + "kind": "actions", + "value_env": "TWITCH_CLIENT_ID", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "TWITCH_CLIENT_ID", + "kind": "dependabot", + "value_env": "TWITCH_CLIENT_ID", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "TWITCH_CLIENT_SECRET", + "kind": "actions", + "value_env": "TWITCH_CLIENT_SECRET", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "TWITCH_CLIENT_SECRET", + "kind": "dependabot", + "value_env": "TWITCH_CLIENT_SECRET", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "VIRUSTOTAL_API_KEY", + "kind": "actions", + "value_env": "VIRUSTOTAL_API_KEY", + "visibility": "selected", + "repository_names": [ + "Koko" + ], + "organizations": [ + "KokoStreamTV" + ], + "missing_repositories": "defer" + }, + { + "name": "VIRUSTOTAL_API_KEY", + "kind": "actions", + "value_env": "VIRUSTOTAL_API_KEY", + "visibility": "selected", + "repository_names": [ + "Sunshine" + ], + "organizations": [ + "SunshineStream" + ], + "missing_repositories": "defer" + }, + { + "name": "VIRUSTOTAL_API_KEY", + "kind": "actions", + "value_env": "VIRUSTOTAL_API_KEY", + "visibility": "selected", + "repository_names": [ + "Themerr" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "YOUTUBE_API_KEY", + "kind": "actions", + "value_env": "YOUTUBE_API_KEY", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + }, + { + "name": "YOUTUBE_API_KEY", + "kind": "dependabot", + "value_env": "YOUTUBE_API_KEY", + "visibility": "selected", + "repository_names": [ + "ThemerrDB" + ], + "organizations": [ + "Themerr" + ], + "missing_repositories": "defer" + } + ], + "webhooks": [ + { + "id": "discord", + "source_url_prefix": "https://discord.com/api/webhooks/", + "unsigned": true + } + ] + } } diff --git a/.github/org-sync.md b/.github/org-sync.md index e869b4ae..44ea15c2 100644 --- a/.github/org-sync.md +++ b/.github/org-sync.md @@ -1,8 +1,9 @@ # Organization administration The Global replicator invokes `org_sync` once for both administration and files. Its -[policy](org-sync.json) uses LizardByte as the source and explicitly authorizes the other four organizations -in [organizations.json](../organizations.json) as destinations. Pull requests preview every operation; +[single policy](org-sync.json) contains `files`, `properties`, and `administration` sections. LizardByte is the +administration source; all other orgs come directly from [organizations.json](../organizations.json), with no +second target list. Pull requests preview every operation; master pushes, the daily schedule, and manual organization/full runs deliver them. Exact-repository runs replicate files only. Administrative failure prevents file delivery in that invocation. Dependabot-triggered runs retain file synchronization but omit administration: GitHub withholds normal Actions @@ -12,7 +13,7 @@ secrets from those runs, and the organization-admin credential is not granted to | Resource | Policy | | ------------------------ | ------------------------------------------------------------------------------------------ | -| Custom-property schemas | Reconcile `repository-properties.json` across all five orgs; preserve repository values | +| Custom-property schemas | Reconcile the policy's `properties` across all registered orgs; preserve repository values | | Shared org settings | Copy 15 shared fields; retain each org's company, name, description, and website | | Actions settings | Copy permissions, workflow defaults, and retention | | Blocked users | Add LizardByte's blocked users; retain destination-only blocks | @@ -93,11 +94,12 @@ only when values or access differ. Missing supplied values fail delivery rather administer every destination and read the source. Classic tokens need `admin:org`; webhook management additionally needs `admin:org_hook`. File delivery still needs repository, workflow, and pull-request write access. GitHub App tokens need the API-specific organization permissions -listed in the [action guide](https://github.com/LizardByte/actions/blob/d4c3996b16bc81b160b02c22958150d1724b6f89/actions/org_sync/administration.md). +listed in the [action guide](https://github.com/LizardByte/actions/blob/a64af5623b9cd9ce51f2e3570055362a15dd9bfc/actions/org_sync/administration.md). Being an ordinary organization member is insufficient for many administrative operations. -The explicit destination lists prevent registry additions from distributing credentials automatically. -Schema provisioning still follows the registry. Neither `ORG_SYNC_ADMIN_TOKEN` nor `GH_PAT` is replicated. +Explicit credential destination lists prevent registry additions from distributing credentials automatically. +File discovery, schemas, and administration follow the registry; administration excludes the source org. +Neither `ORG_SYNC_ADMIN_TOKEN` nor `GH_PAT` is replicated. `GH_BOT_TOKEN` is supplied only to its audited consumers. No secret values or variable values are committed. ## Discord webhooks diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index 52e2b303..6c3b9890 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -37,6 +37,8 @@ jobs: permissions: contents: read runs-on: ubuntu-latest + env: + ORG_SYNC_CONFIG: .github/org-sync.json steps: - name: Checkout source policy and files uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -45,7 +47,8 @@ jobs: - name: Synchronize organizations and shared files id: sync - uses: LizardByte/actions/actions/org_sync@2fedf66b189e56d3f0fc3ba08ec1a0280e027d7e # v2026.1107.44802 + # TODO: Pin the next released version of LizardByte/actions. + uses: LizardByte/actions/actions/org_sync@a64af5623b9cd9ce51f2e3570055362a15dd9bfc env: APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }} APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64: >- @@ -81,14 +84,14 @@ jobs: with: admin_config: >- ${{ inputs.repository == '' && github.actor != 'dependabot[bot]' && - '.github/org-sync.json' || '' }} + env.ORG_SYNC_CONFIG || '' }} admin_token: ${{ secrets.ORG_SYNC_ADMIN_TOKEN || secrets.GH_BOT_TOKEN }} - config: .github/file-sync.json + config: ${{ env.ORG_SYNC_CONFIG }} organizations_file: organizations.json organization: ${{ inputs.organization }} repository: ${{ inputs.repository }} properties_file: >- ${{ inputs.repository == '' && github.actor != 'dependabot[bot]' && - 'repository-properties.json' || '' }} + env.ORG_SYNC_CONFIG || '' }} dry_run: ${{ github.event_name == 'pull_request' }} token: ${{ secrets.GH_BOT_TOKEN }} diff --git a/repository-properties.json b/repository-properties.json deleted file mode 100644 index 8634dcb3..00000000 --- a/repository-properties.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "properties": [ - { - "property_name": "sync_mode", - "value_type": "single_select", - "required": true, - "default_value": "included", - "description": "Shared file synchronization: included by default; excluded opts the repository out.", - "allowed_values": [ - "included", - "excluded" - ], - "values_editable_by": "org_actors", - "require_explicit_values": false - }, - { - "property_name": "workflow_profiles", - "value_type": "multi_select", - "required": false, - "default_value": null, - "description": "Additional shared workflow and configuration groups. No selection means general replication only.", - "allowed_values": [ - "cpm", - "cpp_style", - "custom_issues", - "docker", - "docs", - "flathub", - "homebrew", - "node_ci", - "npm", - "pacman", - "python_style", - "release_notifications", - "winget" - ], - "values_editable_by": "org_actors", - "require_explicit_values": false - } - ] -} diff --git a/tests/file-sync-policy.test.mjs b/tests/file-sync-policy.test.mjs index 7d146094..8d1632ab 100644 --- a/tests/file-sync-policy.test.mjs +++ b/tests/file-sync-policy.test.mjs @@ -2,9 +2,10 @@ import assert from 'node:assert/strict'; import {existsSync, readFileSync, readdirSync} from 'node:fs'; import test from 'node:test'; -const policy = JSON.parse(readFileSync('.github/file-sync.json', 'utf8')); +const configuration = JSON.parse(readFileSync('.github/org-sync.json', 'utf8')); +const policy = {...configuration.files, version: configuration.version}; const organizations = JSON.parse(readFileSync('organizations.json', 'utf8')); -const schema = JSON.parse(readFileSync('repository-properties.json', 'utf8')); +const schema = {properties: configuration.properties}; const workflow = readFileSync('.github/workflows/__global-replicator.yml', 'utf8'); const compareText = (left, right) => left.localeCompare(right); @@ -37,6 +38,8 @@ test('cleanup remains explicit and the obsolete Docker wrapper is never a source test('configured sources exist and policy selectors use the shared organization list and property schema', () => { assert.equal(policy.version, 1); + assert.equal(existsSync('.github/file-sync.json'), false); + assert.equal(existsSync('repository-properties.json'), false); assert.ok(organizations.every(org => typeof org === 'string')); assert.equal(new Set(organizations).size, organizations.length); assert.deepEqual(organizations, organizations.toSorted(compareText)); diff --git a/tests/org-sync-policy.test.mjs b/tests/org-sync-policy.test.mjs index e857bc7e..6b9ca9b3 100644 --- a/tests/org-sync-policy.test.mjs +++ b/tests/org-sync-policy.test.mjs @@ -2,16 +2,18 @@ import assert from 'node:assert/strict'; import {readFileSync} from 'node:fs'; import test from 'node:test'; -const policy = JSON.parse(readFileSync('.github/org-sync.json', 'utf8')); +const configuration = JSON.parse(readFileSync('.github/org-sync.json', 'utf8')); +const policy = configuration.administration; const organizations = JSON.parse(readFileSync('organizations.json', 'utf8')); const workflow = readFileSync('.github/workflows/__global-replicator.yml', 'utf8'); const audit = JSON.parse(readFileSync('tests/fixtures/org-sync-consumers.json', 'utf8')); const compareText = (left, right) => left.localeCompare(right); +const destinations = organizations.filter(org => org !== policy.source_organization); test('the shared policy administers destination orgs while preserving project identities', () => { assert.equal(policy.source_organization, 'LizardByte'); - assert.deepEqual(policy.organizations, organizations.filter(org => org !== policy.source_organization)); - assert.ok(!policy.organizations.includes(policy.source_organization)); + assert.equal(policy.organizations, undefined); + assert.ok(!destinations.includes(policy.source_organization)); assert.deepEqual(policy.settings.from_source, policy.settings.from_source.toSorted(compareText)); for (const field of ['company', 'name', 'description', 'blog']) assert.ok(!policy.settings.from_source.includes(field)); for (const field of ['location', 'twitter_username', 'default_repository_permission', 'members_can_create_repositories']) { @@ -42,7 +44,7 @@ test('credential distribution grants access only to audited active consumers, in assert.equal(entry.missing_repositories, 'defer'); assert.ok(!entry.name.startsWith('GITHUB_')); assert.equal(entry.organizations.length, 1); - assert.ok(entry.organizations.every(org => policy.organizations.includes(org))); + assert.ok(entry.organizations.every(org => destinations.includes(org))); assert.ok(entry.repository_names.length > 0); assert.ok(!entry.repository_names.includes('.github')); } @@ -96,9 +98,12 @@ test('one action invocation handles schemas and policy while exact-repository di assert.equal([...workflow.matchAll(/uses: LizardByte\/actions\/actions\/org_sync@/g)].length, 1); assert.equal([...workflow.slice(workflow.indexOf('\njobs:')).matchAll(/^ [\w-]+:\r?$/gm)].length, 1); const folded = workflow.replaceAll(/\s+/g, ' '); - for (const [input, path] of [['admin_config', '.github/org-sync.json'], ['properties_file', 'repository-properties.json']]) { - assert.ok(folded.includes(`${input}: >- ` + "${{ inputs.repository == '' && github.actor != 'dependabot[bot]' && '" - + path + "' || '' }}")); + assert.ok(workflow.includes('ORG_SYNC_CONFIG: .github/org-sync.json')); + assert.ok(workflow.includes('config: ${{ env.ORG_SYNC_CONFIG }}')); + assert.equal([...workflow.matchAll(/\.github\/org-sync\.json/g)].length, 1); + for (const input of ['admin_config', 'properties_file']) { + assert.ok(folded.includes(`${input}: >- ` + + "${{ inputs.repository == '' && github.actor != 'dependabot[bot]' && env.ORG_SYNC_CONFIG || '' }}")); } assert.ok(workflow.includes('admin_token: ${{ secrets.ORG_SYNC_ADMIN_TOKEN || secrets.GH_BOT_TOKEN }}')); assert.ok(workflow.includes("dry_run: ${{ github.event_name == 'pull_request' }}")); From 58473f02d846124b4a1cba0d9f8ecbf0cd779985 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 7 Nov 2026 00:22:31 -0500 Subject: [PATCH 16/22] ci: refresh the single-policy action pin --- .github/org-sync.md | 2 +- .github/workflows/__global-replicator.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/org-sync.md b/.github/org-sync.md index 44ea15c2..98d49515 100644 --- a/.github/org-sync.md +++ b/.github/org-sync.md @@ -94,7 +94,7 @@ only when values or access differ. Missing supplied values fail delivery rather administer every destination and read the source. Classic tokens need `admin:org`; webhook management additionally needs `admin:org_hook`. File delivery still needs repository, workflow, and pull-request write access. GitHub App tokens need the API-specific organization permissions -listed in the [action guide](https://github.com/LizardByte/actions/blob/a64af5623b9cd9ce51f2e3570055362a15dd9bfc/actions/org_sync/administration.md). +listed in the [action guide](https://github.com/LizardByte/actions/blob/171131a01b2f20d9962813d7eec697530906ab38/actions/org_sync/administration.md). Being an ordinary organization member is insufficient for many administrative operations. Explicit credential destination lists prevent registry additions from distributing credentials automatically. diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index 6c3b9890..09b9aa41 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -48,7 +48,7 @@ jobs: - name: Synchronize organizations and shared files id: sync # TODO: Pin the next released version of LizardByte/actions. - uses: LizardByte/actions/actions/org_sync@a64af5623b9cd9ce51f2e3570055362a15dd9bfc + uses: LizardByte/actions/actions/org_sync@171131a01b2f20d9962813d7eec697530906ab38 env: APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }} APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64: >- From 19fea231ca5c065e8c409240c228af71d752c4a9 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 7 Nov 2026 00:26:37 -0500 Subject: [PATCH 17/22] ci: keep release tracking in the pull request description --- .github/workflows/__global-replicator.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index 09b9aa41..1dac0220 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -47,7 +47,6 @@ jobs: - name: Synchronize organizations and shared files id: sync - # TODO: Pin the next released version of LizardByte/actions. uses: LizardByte/actions/actions/org_sync@171131a01b2f20d9962813d7eec697530906ab38 env: APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }} From ed80840ca72ec98e754f783fbd2a10e2878fba4a Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 7 Nov 2026 08:46:10 -0500 Subject: [PATCH 18/22] ci: sync organization credentials without managing repository access --- .github/file-sync.md | 3 +- .github/org-sync.json | 583 +++------------------- .github/org-sync.md | 155 +++--- .github/workflows/__global-replicator.yml | 5 +- tests/fixtures/org-sync-consumers.json | 3 + tests/org-sync-policy.test.mjs | 30 +- 6 files changed, 149 insertions(+), 630 deletions(-) diff --git a/.github/file-sync.md b/.github/file-sync.md index 98331063..a4f14064 100644 --- a/.github/file-sync.md +++ b/.github/file-sync.md @@ -42,7 +42,8 @@ normal Actions admin secret to Dependabot runs. The owner credential is not repl inheriting them; the PR preview shows those changes before delivery. - Administration targets come directly from `organizations.json`, excluding `source_organization` (LizardByte). There is no duplicate administration org list. Adding an org enables its shared settings and property schema; - credentials still require explicit recipients. See [organization administration](org-sync.md) for details. + credentials still require explicit destination organizations. New organization credentials default to all-repo + access; value updates preserve manually configured access. See [organization administration](org-sync.md). - Public, unarchived repositories are eligible by default, including forks. The source repository is skipped. Existing mirror and Upptime exclusions in LizardByte-infrastructure apply through their properties. - Deselected profiles remove their configured files on the next run. General files remain, and another selected diff --git a/.github/org-sync.json b/.github/org-sync.json index 00156580..966a1437 100644 --- a/.github/org-sync.json +++ b/.github/org-sync.json @@ -388,159 +388,93 @@ { "name": "AZURE_SIGNING_ACCOUNT", "from_source": "AZURE_SIGNING_ACCOUNT", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_SIGNING_ACCOUNT", "from_source": "AZURE_SIGNING_ACCOUNT", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_SIGNING_ACCOUNT", "from_source": "AZURE_SIGNING_ACCOUNT", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_SIGNING_CERT_PROFILE", "from_source": "AZURE_SIGNING_CERT_PROFILE", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_SIGNING_CERT_PROFILE", "from_source": "AZURE_SIGNING_CERT_PROFILE", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_SIGNING_CERT_PROFILE", "from_source": "AZURE_SIGNING_CERT_PROFILE", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_SIGNING_ENDPOINT", "from_source": "AZURE_SIGNING_ENDPOINT", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_SIGNING_ENDPOINT", "from_source": "AZURE_SIGNING_ENDPOINT", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_SIGNING_ENDPOINT", "from_source": "AZURE_SIGNING_ENDPOINT", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "DOCKER_HUB_USERNAME", "from_source": "DOCKER_HUB_USERNAME", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "DOCKER_HUB_USERNAME", "from_source": "DOCKER_HUB_USERNAME", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "GH_BOT_NAME", "from_source": "GH_BOT_NAME", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "GH_BOT_NAME", "from_source": "GH_BOT_NAME", - "visibility": "selected", - "repository_names": [ - "Themerr", - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] } ], "secrets": [ @@ -548,840 +482,465 @@ "name": "APPLE_CODESIGN_IDENTITY", "kind": "actions", "value_env": "APPLE_CODESIGN_IDENTITY", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_CODESIGN_IDENTITY", "kind": "actions", "value_env": "APPLE_CODESIGN_IDENTITY", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_CODESIGN_IDENTITY", "kind": "actions", "value_env": "APPLE_CODESIGN_IDENTITY", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", "kind": "actions", "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", "kind": "actions", "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", "kind": "actions", "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", "kind": "actions", "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", "kind": "actions", "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", "kind": "actions", "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_ID", "kind": "actions", "value_env": "APPLE_ID", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_ID", "kind": "actions", "value_env": "APPLE_ID", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_NOTARYTOOL_PASSWORD", "kind": "actions", "value_env": "APPLE_NOTARYTOOL_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_NOTARYTOOL_PASSWORD", "kind": "actions", "value_env": "APPLE_NOTARYTOOL_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_TEAM_ID", "kind": "actions", "value_env": "APPLE_TEAM_ID", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "APPLE_TEAM_ID", "kind": "actions", "value_env": "APPLE_TEAM_ID", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_CLIENT_ID", "kind": "actions", "value_env": "AZURE_CLIENT_ID", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_CLIENT_ID", "kind": "actions", "value_env": "AZURE_CLIENT_ID", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_CLIENT_ID", "kind": "actions", "value_env": "AZURE_CLIENT_ID", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_CLIENT_SECRET", "kind": "actions", "value_env": "AZURE_CLIENT_SECRET", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_CLIENT_SECRET", "kind": "actions", "value_env": "AZURE_CLIENT_SECRET", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_CLIENT_SECRET", "kind": "actions", "value_env": "AZURE_CLIENT_SECRET", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_TENANT_ID", "kind": "actions", "value_env": "AZURE_TENANT_ID", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_TENANT_ID", "kind": "actions", "value_env": "AZURE_TENANT_ID", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "AZURE_TENANT_ID", "kind": "actions", "value_env": "AZURE_TENANT_ID", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "CLOUDSMITH_API_KEY", "kind": "actions", "value_env": "CLOUDSMITH_API_KEY", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "CODECOV_TOKEN", - "kind": "actions", - "value_env": "ORG_SYNC_CODECOV_KOKOSTREAMTV", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "CODECOV_TOKEN", - "kind": "actions", - "value_env": "ORG_SYNC_CODECOV_SUNSHINESTREAM", - "visibility": "selected", - "repository_names": [ - "awesome-sunshine", - "Sunshine" - ], - "organizations": [ - "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "CODECOV_TOKEN", - "kind": "actions", - "value_env": "ORG_SYNC_CODECOV_THEMERR", - "visibility": "selected", - "repository_names": [ - "Themerr", - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" - }, - { - "name": "CODECOV_TOKEN", - "kind": "dependabot", - "value_env": "ORG_SYNC_CODECOV_KOKOSTREAMTV", - "visibility": "selected", - "repository_names": [ - "Koko" - ], - "organizations": [ - "KokoStreamTV" - ], - "missing_repositories": "defer" - }, - { - "name": "CODECOV_TOKEN", - "kind": "dependabot", - "value_env": "ORG_SYNC_CODECOV_SUNSHINESTREAM", - "visibility": "selected", - "repository_names": [ - "awesome-sunshine", - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" - }, - { - "name": "CODECOV_TOKEN", - "kind": "dependabot", - "value_env": "ORG_SYNC_CODECOV_THEMERR", - "visibility": "selected", - "repository_names": [ - "Themerr", - "ThemerrDB" - ], - "organizations": [ - "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "COPR_BETA_WEBHOOK_TOKEN", "kind": "actions", "value_env": "COPR_BETA_WEBHOOK_TOKEN", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "COPR_CLI_CONFIG", "kind": "actions", "value_env": "COPR_CLI_CONFIG", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "COPR_STABLE_WEBHOOK_TOKEN", "kind": "actions", "value_env": "COPR_STABLE_WEBHOOK_TOKEN", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "CROWDIN_TOKEN", "kind": "actions", "value_env": "CROWDIN_TOKEN", - "visibility": "selected", - "repository_names": [ - "awesome-sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "DOCKER_HUB_ACCESS_TOKEN", "kind": "actions", "value_env": "DOCKER_HUB_ACCESS_TOKEN", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "DOCKER_HUB_ACCESS_TOKEN", "kind": "actions", "value_env": "DOCKER_HUB_ACCESS_TOKEN", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "DOCKER_HUB_PASSWORD", "kind": "actions", "value_env": "DOCKER_HUB_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "DOCKER_HUB_PASSWORD", "kind": "actions", "value_env": "DOCKER_HUB_PASSWORD", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "FANART_PROJECT_API_KEY", "kind": "actions", "value_env": "FANART_PROJECT_API_KEY", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "GH_BOT_EMAIL", "kind": "actions", "value_env": "GH_BOT_EMAIL", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "GH_BOT_EMAIL", "kind": "actions", "value_env": "GH_BOT_EMAIL", - "visibility": "selected", - "repository_names": [ - "Themerr", - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "GH_BOT_EMAIL", "kind": "dependabot", "value_env": "GH_BOT_EMAIL", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "GH_BOT_EMAIL", "kind": "dependabot", "value_env": "GH_BOT_EMAIL", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "GH_BOT_TOKEN", "kind": "actions", "value_env": "GH_BOT_TOKEN", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "GH_BOT_TOKEN", "kind": "actions", "value_env": "GH_BOT_TOKEN", - "visibility": "selected", - "repository_names": [ - "awesome-sunshine", - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "GH_BOT_TOKEN", "kind": "actions", "value_env": "GH_BOT_TOKEN", - "visibility": "selected", - "repository_names": [ - "Themerr", - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "GH_BOT_TOKEN", "kind": "dependabot", "value_env": "GH_BOT_TOKEN", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "GH_BOT_TOKEN", "kind": "dependabot", "value_env": "GH_BOT_TOKEN", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "LRCLIB_APPLICATION_ID", "kind": "actions", "value_env": "LRCLIB_APPLICATION_ID", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "READTHEDOCS_TOKEN", "kind": "actions", "value_env": "READTHEDOCS_TOKEN", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "READTHEDOCS_TOKEN", "kind": "actions", "value_env": "READTHEDOCS_TOKEN", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "READTHEDOCS_TOKEN", "kind": "actions", "value_env": "READTHEDOCS_TOKEN", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "TMDB_API_KEY_V3", "kind": "actions", "value_env": "TMDB_API_KEY_V3", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "TMDB_API_KEY_V3", "kind": "dependabot", "value_env": "TMDB_API_KEY_V3", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "TWITCH_CLIENT_ID", "kind": "actions", "value_env": "TWITCH_CLIENT_ID", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "TWITCH_CLIENT_ID", "kind": "dependabot", "value_env": "TWITCH_CLIENT_ID", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "TWITCH_CLIENT_SECRET", "kind": "actions", "value_env": "TWITCH_CLIENT_SECRET", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "TWITCH_CLIENT_SECRET", "kind": "dependabot", "value_env": "TWITCH_CLIENT_SECRET", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "VIRUSTOTAL_API_KEY", "kind": "actions", "value_env": "VIRUSTOTAL_API_KEY", - "visibility": "selected", - "repository_names": [ - "Koko" - ], "organizations": [ "KokoStreamTV" - ], - "missing_repositories": "defer" + ] }, { "name": "VIRUSTOTAL_API_KEY", "kind": "actions", "value_env": "VIRUSTOTAL_API_KEY", - "visibility": "selected", - "repository_names": [ - "Sunshine" - ], "organizations": [ "SunshineStream" - ], - "missing_repositories": "defer" + ] }, { "name": "VIRUSTOTAL_API_KEY", "kind": "actions", "value_env": "VIRUSTOTAL_API_KEY", - "visibility": "selected", - "repository_names": [ - "Themerr" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "YOUTUBE_API_KEY", "kind": "actions", "value_env": "YOUTUBE_API_KEY", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] }, { "name": "YOUTUBE_API_KEY", "kind": "dependabot", "value_env": "YOUTUBE_API_KEY", - "visibility": "selected", - "repository_names": [ - "ThemerrDB" - ], "organizations": [ "Themerr" - ], - "missing_repositories": "defer" + ] } ], "webhooks": [ diff --git a/.github/org-sync.md b/.github/org-sync.md index 98d49515..eb39f1b7 100644 --- a/.github/org-sync.md +++ b/.github/org-sync.md @@ -11,17 +11,17 @@ secrets from those runs, and the organization-admin credential is not granted to ## Managed configuration -| Resource | Policy | -| ------------------------ | ------------------------------------------------------------------------------------------ | -| Custom-property schemas | Reconcile the policy's `properties` across all registered orgs; preserve repository values | -| Shared org settings | Copy 15 shared fields; retain each org's company, name, description, and website | -| Actions settings | Copy permissions, workflow defaults, and retention | -| Blocked users | Add LizardByte's blocked users; retain destination-only blocks | -| Security configurations | Copy `strict` and `private/internal repositories`, including their new-repository defaults | -| Actions variables | Copy five shared variable names only to their audited consumers | -| Actions secrets | Supply 26 shared secret names plus destination-specific Codecov tokens | -| Dependabot secrets | Supply only the audited pull-request consumers below | -| Organization webhooks | Copy LizardByte's Discord hooks by URL prefix; exclude CLA Assistant | +| Resource | Policy | +| ----------------------- | ---------------------------------------------------------------------------- | +| Custom-property schemas | Reconcile definitions across the registry; preserve repository values | +| Shared org settings | Copy supported shared fields; retain company, name, description, and website | +| Actions settings | Copy permissions, workflow defaults, and retention | +| Blocked users | Add source blocked users; retain destination-only blocks | +| Security configurations | Copy public and private/internal configurations and new-repository defaults | +| Actions variables | Copy names required by active projects to their destination orgs | +| Actions secrets | Supply values required by active projects to their destination orgs | +| Dependabot secrets | Supply values required by active pull-request workflows | +| Organization webhooks | Copy source Discord hooks by URL prefix; exclude CLA Assistant | Each project org retains its current company, name, description, and website. `company` is a separate profile field; the organization display name uses `name`. Other shared profile fields, supported @@ -36,77 +36,45 @@ feature availability and enterprise licensing still apply. ## Credentials and access -The allowlists come from the five active moving repositories and the shared workflows they call, including -Docker, Homebrew, documentation, Pages, Copr, localization, and screenshot publishing. The -[audit fixture](../tests/fixtures/org-sync-consumers.json) records source commits and exact credential names. -Tests reject missing or extra recipients against that audit. Archived Themerr-jellyfin and Themerr-kodi do -not receive credentials. Neither LizardByte-infrastructure nor destination `.github` repositories are recipients. - -| Consumer | Variables | Actions secrets | Dependabot secrets | -| ------------------------------- | --------- | --------------- | ------------------ | -| SunshineStream/Sunshine | 5 | 20 | 3 | -| SunshineStream/awesome-sunshine | 0 | 3 | 1 | -| Themerr/Themerr | 5 | 13 | 1 | -| Themerr/ThemerrDB | 1 | 7 | 7 | -| KokoStreamTV/Koko | 3 | 15 | 1 | - -All entries use `selected` visibility and explicit destination organization/repository names. Same-name -entries may have different scopes or values for disjoint orgs. No credentials are granted org-wide or to the -temporary Pages test repository. `missing_repositories: defer` postpones an entry when none of its named -recipients exist; later runs grant only the named repositories that have arrived. Remaining recipients produce -warnings until transferred. Existing credentials are untouched while an entire entry is deferred. - -The five shared variables are `AZURE_SIGNING_ACCOUNT`, `AZURE_SIGNING_CERT_PROFILE`, `AZURE_SIGNING_ENDPOINT`, -`DOCKER_HUB_USERNAME`, and `GH_BOT_NAME`. Each targets only its consumers. `READTHEDOCS_SLUG` is a project-specific -repository override and is not synchronized from the main org. Source-only social, AUR, CLA, GitLab rotation, -logo/color, and monitoring credentials are excluded from the policy and supplied environment. - -Dependabot receives the same Codecov credential as Actions for each active repo. Sunshine additionally needs -the bot token/email for required Homebrew and Pages workflow inputs. ThemerrDB needs its bot token/email for -Pages plus the four API keys used by its database Pages workflow. Release-only signing/publishing credentials -are not copied into Dependabot. Shared secret values come from explicit Actions secret bindings, never API -plaintext exports or speculative copies of every source-org secret. - -`awesome-sunshine` receives only `CROWDIN_TOKEN`, `GH_BOT_TOKEN`, and SunshineStream's own `CODECOV_TOKEN` -in Actions, plus `CODECOV_TOKEN` in Dependabot. Its Crowdin sync runs only for the translation bot or manual -dispatch, and its GitHub bot token is used by the release job, so neither is granted to Dependabot. It has no -shared variable consumers. Update its Crowdin workflow's LizardByte-only owner guard and the shared Node CI -coverage guards before transfer, and retain the `node_ci` workflow profile. - -Codecov upload tokens have repository/organization scope. The source org's `CODECOV_TOKEN` is not copied. -Each destination's own token is supplied through a dedicated controller binding and written as `CODECOV_TOKEN` -only for that org's audited recipients, in both the Actions and Dependabot namespaces: - -| Destination org | Controller secret binding | -| --------------- | -------------------------------------- | -| KokoStreamTV | `ORG_SYNC_CODECOV_KOKOSTREAMTV` | -| SunshineStream | `ORG_SYNC_CODECOV_SUNSHINESTREAM` | -| Themerr | `ORG_SYNC_CODECOV_THEMERR` | - -See [Codecov's token scopes](https://docs.codecov.com/docs/codecov-tokens). Configure these controller secrets -with tokens from the corresponding Codecov orgs, rather than reusing LizardByte's global token. - -Every delivery resends configured secret values because GitHub cannot compare them. Variable writes occur -only when values or access differ. Missing supplied values fail delivery rather than clearing a secret. - -`ORG_SYNC_ADMIN_TOKEN` can provide a separate administration credential; otherwise the action uses -`GH_BOT_TOKEN`. The previous `GH_PAT` fallback returned HTTP 401 and is no longer used. The credential must -administer every destination and read the source. Classic tokens need -`admin:org`; webhook management additionally needs `admin:org_hook`. File delivery still needs repository, -workflow, and pull-request write access. GitHub App tokens need the API-specific organization permissions -listed in the [action guide](https://github.com/LizardByte/actions/blob/171131a01b2f20d9962813d7eec697530906ab38/actions/org_sync/administration.md). -Being an ordinary organization member is insufficient for many administrative operations. - -Explicit credential destination lists prevent registry additions from distributing credentials automatically. -File discovery, schemas, and administration follow the registry; administration excludes the source org. -Neither `ORG_SYNC_ADMIN_TOKEN` nor `GH_PAT` is replicated. -`GH_BOT_TOKEN` is supplied only to its audited consumers. No secret values or variable values are committed. +All synced secrets and variables belong to the destination **organization**. Repository-level copies are not +created. Each entry names its destination orgs; adding an org to the shared registry does not automatically +distribute credentials to it. + +The policy omits access fields. New credentials are created with access to **all repositories** in their +destination org. Later value updates preserve the existing visibility and selected-repository list. You can +restrict access manually in the destination organization settings, and subsequent runs retain your choices. +Credentials can be provisioned before project repositories transfer. + +The [audit fixture](../tests/fixtures/org-sync-consumers.json) records the active moving projects, their source +commits, and the credential names needed by their own and reusable workflows. Tests compare the union of those +requirements for each destination org against the policy. Archived projects do not contribute requirements. +LizardByte-infrastructure has no credential entries. Repository exclusions from file sync do not restrict +access to organization credentials. + +Configure each destination org's `CODECOV_TOKEN` manually in its Actions and Dependabot secrets as required by +its workflows. Codecov tokens are outside the sync policy and have no controller bindings. + +Shared variables are copied by name from LizardByte. `READTHEDOCS_SLUG` remains a project-specific repository +override. Shared secret values come from explicit workflow secret bindings; GitHub cannot export stored secret +plaintext. Source-only social, AUR, CLA, GitLab rotation, branding, and monitoring credentials are excluded. +Dependabot entries contain only names required by the active pull-request workflows. + +Every delivery resends configured secret values to support rotation. Variables are updated only when their +value changes. Missing supplied values fail delivery rather than clearing a secret. Omitting a credential +from the policy does not delete it from a destination. + +`ORG_SYNC_ADMIN_TOKEN` supplies the administration credential when configured; otherwise the workflow uses +`GH_BOT_TOKEN`. It must administer the destinations and read the source. Classic tokens need `admin:org`; +webhook management also needs `admin:org_hook`. File delivery needs repository, workflow, and pull-request +write access. GitHub App tokens need the API-specific organization permissions listed in the +[action guide](https://github.com/LizardByte/actions/blob/cbd8b120432599b6b1e49b52b05c478445fba13b/actions/org_sync/administration.md). +`ORG_SYNC_ADMIN_TOKEN` is controller-only and is not replicated. ## Discord webhooks The action selects `web` hooks under `https://discord.com/api/webhooks/` from the source org at runtime, then copies each hook's receiver URL, event subscriptions, activity, and content type to the destinations. This -includes the six Discord hooks shown in the source settings and follows future matching hooks automatically. +follows matching source hooks automatically. CLA Assistant is outside this prefix and is not copied. The Discord URL tokens are never committed or written to the report. Discord receiver authentication is in the URL; `unsigned: true` explicitly configures delivery without a separate GitHub HMAC signing secret. @@ -116,28 +84,17 @@ those existing hooks. GitHub's OAuth webhook restrictions can produce a filtered `admin:org_hook` scope; use a suitable PAT/App credential. An empty selected group fails rather than silently skipping webhook synchronization. PRs only preview the copies; no webhook is created or updated until delivery. -## Remaining bootstrap work - -- The controller must retain access to six source Actions secrets: `FANART_PROJECT_API_KEY` and - `LRCLIB_APPLICATION_ID` for Koko, plus `TMDB_API_KEY_V3`, `TWITCH_CLIENT_ID`, `TWITCH_CLIENT_SECRET`, and - `YOUTUBE_API_KEY` for ThemerrDB. Controller access was provisioned with approval by adding only LizardByte/.github - to those selected-repository lists and verifying that previous recipients remained. No Plex, Patreon, social, - or unrelated API credentials are needed by this audit. -- Controller access to `CROWDIN_TOKEN` for `awesome-sunshine` was provisioned with approval: LizardByte/.github - was added as the fifth selected source recipient, and all four previous recipients were preserved. Destination - access remains limited to `SunshineStream/awesome-sunshine`. -- Supply the three destination-specific Codecov bindings above before their recipients transfer. Once a - recipient exists, missing supplied credentials fail delivery rather than silently creating an unusable setup. -- Supply `ORG_SYNC_ADMIN_TOKEN` with organization-administration and webhook API access if the bot token lacks - those permissions. The current bot token returned HTTP 403 when reading Actions settings, despite the bot's - verified owner membership. A classic PAT from an owner of all five orgs needs `admin:org` and `admin:org_hook`. -- Some source workflows still hard-code the LizardByte owner or old publishing locations. Update those as part - of transfer preparation; this policy covers their intended consumers after the owner guards are corrected. -- Codespaces organization-secret discovery returned HTTP 404, so no Codespaces secrets are assumed or configured. -- Additional receivers outside the Discord prefix need reviewed `webhooks` entries and, when applicable, - supplied signing-secret bindings. No separate manual webhook replication runs alongside the action. -- Organization default labels/default branch names, extra social links, package policies, standalone-org 2FA, - and PAT allow/approval/lifetime policies require manual setup because their public write APIs are unavailable. +## Transfer prerequisites + +- Ensure the controller can read every source secret referenced by the workflow, including project API keys + and Crowdin. Preserve existing source access when granting the controller access. +- Configure destination Codecov credentials manually and choose repository access in each org as needed. +- Ensure the administration credential has the required permissions across the registered orgs. +- Update project/shared workflow owner guards and publishing locations before transfer, including Crowdin + and shared Node CI for `awesome-sunshine`; retain its `node_ci` profile. +- Review additional webhook receivers and supply signing secrets when required. +- Set organization default labels/default branch names, extra social links, package policies, standalone-org + 2FA, and PAT policies manually where supported write APIs are unavailable. Review the combined report's organization before/after settings, schema defaults, credential access counts, and per-operation status before merging. Delivery is not transactional: a failed later write can leave earlier diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index 1dac0220..6dcfc3f7 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -47,7 +47,7 @@ jobs: - name: Synchronize organizations and shared files id: sync - uses: LizardByte/actions/actions/org_sync@171131a01b2f20d9962813d7eec697530906ab38 + uses: LizardByte/actions/actions/org_sync@cbd8b120432599b6b1e49b52b05c478445fba13b env: APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }} APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64: >- @@ -71,9 +71,6 @@ jobs: GH_BOT_EMAIL: ${{ secrets.GH_BOT_EMAIL }} GH_BOT_TOKEN: ${{ secrets.GH_BOT_TOKEN }} LRCLIB_APPLICATION_ID: ${{ secrets.LRCLIB_APPLICATION_ID }} - ORG_SYNC_CODECOV_KOKOSTREAMTV: ${{ secrets.ORG_SYNC_CODECOV_KOKOSTREAMTV }} - ORG_SYNC_CODECOV_SUNSHINESTREAM: ${{ secrets.ORG_SYNC_CODECOV_SUNSHINESTREAM }} - ORG_SYNC_CODECOV_THEMERR: ${{ secrets.ORG_SYNC_CODECOV_THEMERR }} READTHEDOCS_TOKEN: ${{ secrets.READTHEDOCS_TOKEN }} TMDB_API_KEY_V3: ${{ secrets.TMDB_API_KEY_V3 }} TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }} diff --git a/tests/fixtures/org-sync-consumers.json b/tests/fixtures/org-sync-consumers.json index d52e645d..3cc51ad9 100644 --- a/tests/fixtures/org-sync-consumers.json +++ b/tests/fixtures/org-sync-consumers.json @@ -1,4 +1,7 @@ { + "manual_credentials": [ + "CODECOV_TOKEN" + ], "repositories": [ { "repository": "SunshineStream/Sunshine", diff --git a/tests/org-sync-policy.test.mjs b/tests/org-sync-policy.test.mjs index 6b9ca9b3..59a7330c 100644 --- a/tests/org-sync-policy.test.mjs +++ b/tests/org-sync-policy.test.mjs @@ -36,27 +36,24 @@ test('Discord hooks are selected at runtime without credential URLs or unrelated assert.doesNotMatch(JSON.stringify(policy.webhooks), /cla/i); }); -test('credential distribution grants access only to audited active consumers, including future transferred repos', () => { +test('organization credential names match active project requirements without managing repository access', () => { const identities = policy.secrets.flatMap(entry => entry.organizations.map(org => `${org}/${entry.kind}/${entry.name}`)); assert.equal(new Set(identities).size, identities.length); for (const entry of [...policy.variables, ...policy.secrets]) { - assert.equal(entry.visibility, 'selected'); - assert.equal(entry.missing_repositories, 'defer'); + for (const field of ['visibility', 'repository_names', 'missing_repositories']) assert.equal(entry[field], undefined); assert.ok(!entry.name.startsWith('GITHUB_')); assert.equal(entry.organizations.length, 1); assert.ok(entry.organizations.every(org => destinations.includes(org))); - assert.ok(entry.repository_names.length > 0); - assert.ok(!entry.repository_names.includes('.github')); } const actual = [...policy.variables.map(entry => ({...entry, kind: 'variable'})), ...policy.secrets] - .flatMap(entry => entry.organizations.flatMap(org => entry.repository_names.map(repo => `${org}/${repo}:${entry.kind}:${entry.name}`))); - const expected = audit.repositories.flatMap(repo => [ - ...repo.variables.map(name => `${repo.repository}:variable:${name}`), - ...repo.actions_secrets.map(name => `${repo.repository}:actions:${name}`), - ...repo.dependabot_secrets.map(name => `${repo.repository}:dependabot:${name}`), - ]); + .flatMap(entry => entry.organizations.map(org => `${org}:${entry.kind}:${entry.name}`)); + const expected = [...new Set(audit.repositories.flatMap(repo => { + const org = repo.repository.split('/')[0]; + return [['variable', repo.variables], ['actions', repo.actions_secrets], ['dependabot', repo.dependabot_secrets]] + .flatMap(([kind, names]) => names.filter(name => !audit.manual_credentials.includes(name)) + .map(name => `${org}:${kind}:${name}`)); + }))]; assert.deepEqual(actual.toSorted(compareText), expected.toSorted(compareText)); - for (const repo of audit.archived_repositories) assert.ok(actual.every(value => !value.startsWith(`${repo}:`))); for (const repo of audit.repositories) { assert.match(repo.source_commit, /^[a-f\d]{40}$/); assert.ok(!repo.repository.startsWith('LizardByte-infrastructure/')); @@ -70,8 +67,7 @@ test('variables are copied by name and secrets use explicit supplied bindings, w assert.equal(entry.value, undefined); } for (const entry of policy.secrets) { - const expectedBinding = entry.name === 'CODECOV_TOKEN' ? `ORG_SYNC_CODECOV_${entry.organizations[0].toUpperCase()}` : entry.name; - assert.equal(entry.value_env, expectedBinding); + assert.equal(entry.value_env, entry.name); assert.equal(entry.value, undefined); assert.equal(entry.from_source, undefined); assert.equal(entry.value_key, undefined); @@ -80,6 +76,12 @@ test('variables are copied by name and secrets use explicit supplied bindings, w assert.ok(!policy.secrets.some(entry => entry.name === 'ORG_SYNC_ADMIN_TOKEN')); }); +test('Codecov credentials are managed manually in destination orgs', () => { + assert.deepEqual(audit.manual_credentials, ['CODECOV_TOKEN']); + assert.ok(!policy.secrets.some(entry => entry.name === 'CODECOV_TOKEN')); + assert.doesNotMatch(workflow, /CODECOV/); +}); + test('controller-only, social, and unused credentials are absent from both the policy and its supplied bindings', () => { const excluded = ['AUR_SSH_PUBLIC_KEY', 'AUR_SSH_PRIVATE_KEY', 'BUFFER_X_CHANNEL_ID', 'BUFFER_API_KEY', 'CLA_GIST_ID', 'COLOR_HEX_GREEN', 'COLOR_HEX_RED', 'DISCORD_GITHUB_STATUS_CHANNEL_ID', 'DISCORD_REDDIT_CHANNEL_ID', From 9026352da7b0371f8d58731a46653148c4d6132a Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 7 Nov 2026 08:49:49 -0500 Subject: [PATCH 19/22] ci: update organization sync action pin --- .github/org-sync.md | 2 +- .github/workflows/__global-replicator.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/org-sync.md b/.github/org-sync.md index eb39f1b7..091ce545 100644 --- a/.github/org-sync.md +++ b/.github/org-sync.md @@ -67,7 +67,7 @@ from the policy does not delete it from a destination. `GH_BOT_TOKEN`. It must administer the destinations and read the source. Classic tokens need `admin:org`; webhook management also needs `admin:org_hook`. File delivery needs repository, workflow, and pull-request write access. GitHub App tokens need the API-specific organization permissions listed in the -[action guide](https://github.com/LizardByte/actions/blob/cbd8b120432599b6b1e49b52b05c478445fba13b/actions/org_sync/administration.md). +[action guide](https://github.com/LizardByte/actions/blob/c18db340531d8d60a9083435cd65c4dd62e60956/actions/org_sync/administration.md). `ORG_SYNC_ADMIN_TOKEN` is controller-only and is not replicated. ## Discord webhooks diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index 6dcfc3f7..80953bd9 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -47,7 +47,7 @@ jobs: - name: Synchronize organizations and shared files id: sync - uses: LizardByte/actions/actions/org_sync@cbd8b120432599b6b1e49b52b05c478445fba13b + uses: LizardByte/actions/actions/org_sync@c18db340531d8d60a9083435cd65c4dd62e60956 env: APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }} APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64: >- From b1ec3e6b58e41939fed2d80c634530821fc9b683 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:48:39 -0500 Subject: [PATCH 20/22] test: define sync credential expectations by organization --- .github/org-sync.md | 5 +- tests/fixtures/org-sync-consumers.json | 109 +++++++------------------ tests/org-sync-policy.test.mjs | 22 ++--- 3 files changed, 41 insertions(+), 95 deletions(-) diff --git a/.github/org-sync.md b/.github/org-sync.md index 091ce545..ff140949 100644 --- a/.github/org-sync.md +++ b/.github/org-sync.md @@ -45,9 +45,8 @@ destination org. Later value updates preserve the existing visibility and select restrict access manually in the destination organization settings, and subsequent runs retain your choices. Credentials can be provisioned before project repositories transfer. -The [audit fixture](../tests/fixtures/org-sync-consumers.json) records the active moving projects, their source -commits, and the credential names needed by their own and reusable workflows. Tests compare the union of those -requirements for each destination org against the policy. Archived projects do not contribute requirements. +The [test fixture](../tests/fixtures/org-sync-consumers.json) lists expected variable and secret names by +destination organization. Tests compare these organization-level expectations against the policy. LizardByte-infrastructure has no credential entries. Repository exclusions from file sync do not restrict access to organization credentials. diff --git a/tests/fixtures/org-sync-consumers.json b/tests/fixtures/org-sync-consumers.json index 3cc51ad9..838dccba 100644 --- a/tests/fixtures/org-sync-consumers.json +++ b/tests/fixtures/org-sync-consumers.json @@ -2,11 +2,32 @@ "manual_credentials": [ "CODECOV_TOKEN" ], - "repositories": [ - { - "repository": "SunshineStream/Sunshine", - "source_repository": "LizardByte/Sunshine", - "source_commit": "010f9564e521cb4f3459795187fe87d0719583bf", + "organizations": { + "KokoStreamTV": { + "variables": [ + "AZURE_SIGNING_ACCOUNT", + "AZURE_SIGNING_CERT_PROFILE", + "AZURE_SIGNING_ENDPOINT" + ], + "actions_secrets": [ + "APPLE_CODESIGN_IDENTITY", + "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", + "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", + "APPLE_ID", + "APPLE_NOTARYTOOL_PASSWORD", + "APPLE_TEAM_ID", + "AZURE_CLIENT_ID", + "AZURE_CLIENT_SECRET", + "AZURE_TENANT_ID", + "FANART_PROJECT_API_KEY", + "GH_BOT_TOKEN", + "LRCLIB_APPLICATION_ID", + "READTHEDOCS_TOKEN", + "VIRUSTOTAL_API_KEY" + ], + "dependabot_secrets": [] + }, + "SunshineStream": { "variables": [ "AZURE_SIGNING_ACCOUNT", "AZURE_SIGNING_CERT_PROFILE", @@ -25,10 +46,10 @@ "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID", "CLOUDSMITH_API_KEY", - "CODECOV_TOKEN", "COPR_BETA_WEBHOOK_TOKEN", "COPR_CLI_CONFIG", "COPR_STABLE_WEBHOOK_TOKEN", + "CROWDIN_TOKEN", "DOCKER_HUB_ACCESS_TOKEN", "DOCKER_HUB_PASSWORD", "GH_BOT_EMAIL", @@ -37,29 +58,11 @@ "VIRUSTOTAL_API_KEY" ], "dependabot_secrets": [ - "CODECOV_TOKEN", "GH_BOT_EMAIL", "GH_BOT_TOKEN" ] }, - { - "repository": "SunshineStream/awesome-sunshine", - "source_repository": "LizardByte/awesome-sunshine", - "source_commit": "ef2364ccab13509f273c25e08f6dd380b8f98be5", - "variables": [], - "actions_secrets": [ - "CODECOV_TOKEN", - "CROWDIN_TOKEN", - "GH_BOT_TOKEN" - ], - "dependabot_secrets": [ - "CODECOV_TOKEN" - ] - }, - { - "repository": "Themerr/Themerr", - "source_repository": "LizardByte/Themerr", - "source_commit": "b1464f27dbc8e5c59740c4e97948a32c183038a3", + "Themerr": { "variables": [ "AZURE_SIGNING_ACCOUNT", "AZURE_SIGNING_CERT_PROFILE", @@ -74,36 +77,18 @@ "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID", - "CODECOV_TOKEN", "DOCKER_HUB_ACCESS_TOKEN", "DOCKER_HUB_PASSWORD", "GH_BOT_EMAIL", "GH_BOT_TOKEN", "READTHEDOCS_TOKEN", - "VIRUSTOTAL_API_KEY" - ], - "dependabot_secrets": [ - "CODECOV_TOKEN" - ] - }, - { - "repository": "Themerr/ThemerrDB", - "source_repository": "LizardByte/ThemerrDB", - "source_commit": "31aadd8e681992357675da10dfa3e29d9c8615fb", - "variables": [ - "GH_BOT_NAME" - ], - "actions_secrets": [ - "CODECOV_TOKEN", - "GH_BOT_EMAIL", - "GH_BOT_TOKEN", "TMDB_API_KEY_V3", "TWITCH_CLIENT_ID", "TWITCH_CLIENT_SECRET", + "VIRUSTOTAL_API_KEY", "YOUTUBE_API_KEY" ], "dependabot_secrets": [ - "CODECOV_TOKEN", "GH_BOT_EMAIL", "GH_BOT_TOKEN", "TMDB_API_KEY_V3", @@ -111,40 +96,6 @@ "TWITCH_CLIENT_SECRET", "YOUTUBE_API_KEY" ] - }, - { - "repository": "KokoStreamTV/Koko", - "source_repository": "LizardByte/Koko", - "source_commit": "3c32f1f9a0b3cd3f385732e42e3e75194e833cbe", - "variables": [ - "AZURE_SIGNING_ACCOUNT", - "AZURE_SIGNING_CERT_PROFILE", - "AZURE_SIGNING_ENDPOINT" - ], - "actions_secrets": [ - "APPLE_CODESIGN_IDENTITY", - "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "APPLE_ID", - "APPLE_NOTARYTOOL_PASSWORD", - "APPLE_TEAM_ID", - "AZURE_CLIENT_ID", - "AZURE_CLIENT_SECRET", - "AZURE_TENANT_ID", - "CODECOV_TOKEN", - "FANART_PROJECT_API_KEY", - "GH_BOT_TOKEN", - "LRCLIB_APPLICATION_ID", - "READTHEDOCS_TOKEN", - "VIRUSTOTAL_API_KEY" - ], - "dependabot_secrets": [ - "CODECOV_TOKEN" - ] } - ], - "archived_repositories": [ - "Themerr/Themerr-jellyfin", - "Themerr/Themerr-kodi" - ] + } } diff --git a/tests/org-sync-policy.test.mjs b/tests/org-sync-policy.test.mjs index 59a7330c..3e3798c2 100644 --- a/tests/org-sync-policy.test.mjs +++ b/tests/org-sync-policy.test.mjs @@ -6,7 +6,7 @@ const configuration = JSON.parse(readFileSync('.github/org-sync.json', 'utf8')); const policy = configuration.administration; const organizations = JSON.parse(readFileSync('organizations.json', 'utf8')); const workflow = readFileSync('.github/workflows/__global-replicator.yml', 'utf8'); -const audit = JSON.parse(readFileSync('tests/fixtures/org-sync-consumers.json', 'utf8')); +const expectedCredentials = JSON.parse(readFileSync('tests/fixtures/org-sync-consumers.json', 'utf8')); const compareText = (left, right) => left.localeCompare(right); const destinations = organizations.filter(org => org !== policy.source_organization); @@ -36,7 +36,7 @@ test('Discord hooks are selected at runtime without credential URLs or unrelated assert.doesNotMatch(JSON.stringify(policy.webhooks), /cla/i); }); -test('organization credential names match active project requirements without managing repository access', () => { +test('organization credential names match destination org expectations without managing repository access', () => { const identities = policy.secrets.flatMap(entry => entry.organizations.map(org => `${org}/${entry.kind}/${entry.name}`)); assert.equal(new Set(identities).size, identities.length); for (const entry of [...policy.variables, ...policy.secrets]) { @@ -47,17 +47,13 @@ test('organization credential names match active project requirements without ma } const actual = [...policy.variables.map(entry => ({...entry, kind: 'variable'})), ...policy.secrets] .flatMap(entry => entry.organizations.map(org => `${org}:${entry.kind}:${entry.name}`)); - const expected = [...new Set(audit.repositories.flatMap(repo => { - const org = repo.repository.split('/')[0]; - return [['variable', repo.variables], ['actions', repo.actions_secrets], ['dependabot', repo.dependabot_secrets]] - .flatMap(([kind, names]) => names.filter(name => !audit.manual_credentials.includes(name)) - .map(name => `${org}:${kind}:${name}`)); - }))]; + const expected = Object.entries(expectedCredentials.organizations).flatMap(([org, credentials]) => { + assert.ok(destinations.includes(org)); + return [['variable', credentials.variables], ['actions', credentials.actions_secrets], + ['dependabot', credentials.dependabot_secrets]] + .flatMap(([kind, names]) => names.map(name => `${org}:${kind}:${name}`)); + }); assert.deepEqual(actual.toSorted(compareText), expected.toSorted(compareText)); - for (const repo of audit.repositories) { - assert.match(repo.source_commit, /^[a-f\d]{40}$/); - assert.ok(!repo.repository.startsWith('LizardByte-infrastructure/')); - } }); test('variables are copied by name and secrets use explicit supplied bindings, with no committed plaintext', () => { @@ -77,7 +73,7 @@ test('variables are copied by name and secrets use explicit supplied bindings, w }); test('Codecov credentials are managed manually in destination orgs', () => { - assert.deepEqual(audit.manual_credentials, ['CODECOV_TOKEN']); + assert.deepEqual(expectedCredentials.manual_credentials, ['CODECOV_TOKEN']); assert.ok(!policy.secrets.some(entry => entry.name === 'CODECOV_TOKEN')); assert.doesNotMatch(workflow, /CODECOV/); }); From f0550dbe79969d1801191cde9302f0bf22c1146f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 7 Nov 2026 09:58:29 -0500 Subject: [PATCH 21/22] refactor: group shared credentials by destination organizations --- .github/org-sync.json | 298 ++++----------------------------- .github/org-sync.md | 1 + tests/org-sync-policy.test.mjs | 3 +- 3 files changed, 37 insertions(+), 265 deletions(-) diff --git a/.github/org-sync.json b/.github/org-sync.json index 966a1437..17f6d1f5 100644 --- a/.github/org-sync.json +++ b/.github/org-sync.json @@ -389,20 +389,8 @@ "name": "AZURE_SIGNING_ACCOUNT", "from_source": "AZURE_SIGNING_ACCOUNT", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "AZURE_SIGNING_ACCOUNT", - "from_source": "AZURE_SIGNING_ACCOUNT", - "organizations": [ - "SunshineStream" - ] - }, - { - "name": "AZURE_SIGNING_ACCOUNT", - "from_source": "AZURE_SIGNING_ACCOUNT", - "organizations": [ + "KokoStreamTV", + "SunshineStream", "Themerr" ] }, @@ -410,20 +398,8 @@ "name": "AZURE_SIGNING_CERT_PROFILE", "from_source": "AZURE_SIGNING_CERT_PROFILE", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "AZURE_SIGNING_CERT_PROFILE", - "from_source": "AZURE_SIGNING_CERT_PROFILE", - "organizations": [ - "SunshineStream" - ] - }, - { - "name": "AZURE_SIGNING_CERT_PROFILE", - "from_source": "AZURE_SIGNING_CERT_PROFILE", - "organizations": [ + "KokoStreamTV", + "SunshineStream", "Themerr" ] }, @@ -431,20 +407,8 @@ "name": "AZURE_SIGNING_ENDPOINT", "from_source": "AZURE_SIGNING_ENDPOINT", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "AZURE_SIGNING_ENDPOINT", - "from_source": "AZURE_SIGNING_ENDPOINT", - "organizations": [ - "SunshineStream" - ] - }, - { - "name": "AZURE_SIGNING_ENDPOINT", - "from_source": "AZURE_SIGNING_ENDPOINT", - "organizations": [ + "KokoStreamTV", + "SunshineStream", "Themerr" ] }, @@ -452,13 +416,7 @@ "name": "DOCKER_HUB_USERNAME", "from_source": "DOCKER_HUB_USERNAME", "organizations": [ - "SunshineStream" - ] - }, - { - "name": "DOCKER_HUB_USERNAME", - "from_source": "DOCKER_HUB_USERNAME", - "organizations": [ + "SunshineStream", "Themerr" ] }, @@ -466,13 +424,7 @@ "name": "GH_BOT_NAME", "from_source": "GH_BOT_NAME", "organizations": [ - "SunshineStream" - ] - }, - { - "name": "GH_BOT_NAME", - "from_source": "GH_BOT_NAME", - "organizations": [ + "SunshineStream", "Themerr" ] } @@ -483,22 +435,8 @@ "kind": "actions", "value_env": "APPLE_CODESIGN_IDENTITY", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "APPLE_CODESIGN_IDENTITY", - "kind": "actions", - "value_env": "APPLE_CODESIGN_IDENTITY", - "organizations": [ - "SunshineStream" - ] - }, - { - "name": "APPLE_CODESIGN_IDENTITY", - "kind": "actions", - "value_env": "APPLE_CODESIGN_IDENTITY", - "organizations": [ + "KokoStreamTV", + "SunshineStream", "Themerr" ] }, @@ -507,22 +445,8 @@ "kind": "actions", "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "kind": "actions", - "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "organizations": [ - "SunshineStream" - ] - }, - { - "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "kind": "actions", - "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64", - "organizations": [ + "KokoStreamTV", + "SunshineStream", "Themerr" ] }, @@ -531,22 +455,8 @@ "kind": "actions", "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "kind": "actions", - "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "organizations": [ - "SunshineStream" - ] - }, - { - "name": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "kind": "actions", - "value_env": "APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_P12_PASSWORD", - "organizations": [ + "KokoStreamTV", + "SunshineStream", "Themerr" ] }, @@ -555,14 +465,7 @@ "kind": "actions", "value_env": "APPLE_ID", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "APPLE_ID", - "kind": "actions", - "value_env": "APPLE_ID", - "organizations": [ + "KokoStreamTV", "SunshineStream" ] }, @@ -571,14 +474,7 @@ "kind": "actions", "value_env": "APPLE_NOTARYTOOL_PASSWORD", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "APPLE_NOTARYTOOL_PASSWORD", - "kind": "actions", - "value_env": "APPLE_NOTARYTOOL_PASSWORD", - "organizations": [ + "KokoStreamTV", "SunshineStream" ] }, @@ -587,30 +483,7 @@ "kind": "actions", "value_env": "APPLE_TEAM_ID", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "APPLE_TEAM_ID", - "kind": "actions", - "value_env": "APPLE_TEAM_ID", - "organizations": [ - "SunshineStream" - ] - }, - { - "name": "AZURE_CLIENT_ID", - "kind": "actions", - "value_env": "AZURE_CLIENT_ID", - "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "AZURE_CLIENT_ID", - "kind": "actions", - "value_env": "AZURE_CLIENT_ID", - "organizations": [ + "KokoStreamTV", "SunshineStream" ] }, @@ -619,6 +492,8 @@ "kind": "actions", "value_env": "AZURE_CLIENT_ID", "organizations": [ + "KokoStreamTV", + "SunshineStream", "Themerr" ] }, @@ -627,22 +502,8 @@ "kind": "actions", "value_env": "AZURE_CLIENT_SECRET", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "AZURE_CLIENT_SECRET", - "kind": "actions", - "value_env": "AZURE_CLIENT_SECRET", - "organizations": [ - "SunshineStream" - ] - }, - { - "name": "AZURE_CLIENT_SECRET", - "kind": "actions", - "value_env": "AZURE_CLIENT_SECRET", - "organizations": [ + "KokoStreamTV", + "SunshineStream", "Themerr" ] }, @@ -651,22 +512,8 @@ "kind": "actions", "value_env": "AZURE_TENANT_ID", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "AZURE_TENANT_ID", - "kind": "actions", - "value_env": "AZURE_TENANT_ID", - "organizations": [ - "SunshineStream" - ] - }, - { - "name": "AZURE_TENANT_ID", - "kind": "actions", - "value_env": "AZURE_TENANT_ID", - "organizations": [ + "KokoStreamTV", + "SunshineStream", "Themerr" ] }, @@ -715,14 +562,7 @@ "kind": "actions", "value_env": "DOCKER_HUB_ACCESS_TOKEN", "organizations": [ - "SunshineStream" - ] - }, - { - "name": "DOCKER_HUB_ACCESS_TOKEN", - "kind": "actions", - "value_env": "DOCKER_HUB_ACCESS_TOKEN", - "organizations": [ + "SunshineStream", "Themerr" ] }, @@ -731,14 +571,7 @@ "kind": "actions", "value_env": "DOCKER_HUB_PASSWORD", "organizations": [ - "SunshineStream" - ] - }, - { - "name": "DOCKER_HUB_PASSWORD", - "kind": "actions", - "value_env": "DOCKER_HUB_PASSWORD", - "organizations": [ + "SunshineStream", "Themerr" ] }, @@ -755,14 +588,7 @@ "kind": "actions", "value_env": "GH_BOT_EMAIL", "organizations": [ - "SunshineStream" - ] - }, - { - "name": "GH_BOT_EMAIL", - "kind": "actions", - "value_env": "GH_BOT_EMAIL", - "organizations": [ + "SunshineStream", "Themerr" ] }, @@ -771,14 +597,7 @@ "kind": "dependabot", "value_env": "GH_BOT_EMAIL", "organizations": [ - "SunshineStream" - ] - }, - { - "name": "GH_BOT_EMAIL", - "kind": "dependabot", - "value_env": "GH_BOT_EMAIL", - "organizations": [ + "SunshineStream", "Themerr" ] }, @@ -787,22 +606,8 @@ "kind": "actions", "value_env": "GH_BOT_TOKEN", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "GH_BOT_TOKEN", - "kind": "actions", - "value_env": "GH_BOT_TOKEN", - "organizations": [ - "SunshineStream" - ] - }, - { - "name": "GH_BOT_TOKEN", - "kind": "actions", - "value_env": "GH_BOT_TOKEN", - "organizations": [ + "KokoStreamTV", + "SunshineStream", "Themerr" ] }, @@ -811,14 +616,7 @@ "kind": "dependabot", "value_env": "GH_BOT_TOKEN", "organizations": [ - "SunshineStream" - ] - }, - { - "name": "GH_BOT_TOKEN", - "kind": "dependabot", - "value_env": "GH_BOT_TOKEN", - "organizations": [ + "SunshineStream", "Themerr" ] }, @@ -835,22 +633,8 @@ "kind": "actions", "value_env": "READTHEDOCS_TOKEN", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "READTHEDOCS_TOKEN", - "kind": "actions", - "value_env": "READTHEDOCS_TOKEN", - "organizations": [ - "SunshineStream" - ] - }, - { - "name": "READTHEDOCS_TOKEN", - "kind": "actions", - "value_env": "READTHEDOCS_TOKEN", - "organizations": [ + "KokoStreamTV", + "SunshineStream", "Themerr" ] }, @@ -907,22 +691,8 @@ "kind": "actions", "value_env": "VIRUSTOTAL_API_KEY", "organizations": [ - "KokoStreamTV" - ] - }, - { - "name": "VIRUSTOTAL_API_KEY", - "kind": "actions", - "value_env": "VIRUSTOTAL_API_KEY", - "organizations": [ - "SunshineStream" - ] - }, - { - "name": "VIRUSTOTAL_API_KEY", - "kind": "actions", - "value_env": "VIRUSTOTAL_API_KEY", - "organizations": [ + "KokoStreamTV", + "SunshineStream", "Themerr" ] }, diff --git a/.github/org-sync.md b/.github/org-sync.md index ff140949..9c21f212 100644 --- a/.github/org-sync.md +++ b/.github/org-sync.md @@ -39,6 +39,7 @@ feature availability and enterprise licensing still apply. All synced secrets and variables belong to the destination **organization**. Repository-level copies are not created. Each entry names its destination orgs; adding an org to the shared registry does not automatically distribute credentials to it. +Credentials with the same source and settings share one entry listing all their destination organizations. The policy omits access fields. New credentials are created with access to **all repositories** in their destination org. Later value updates preserve the existing visibility and selected-repository list. You can diff --git a/tests/org-sync-policy.test.mjs b/tests/org-sync-policy.test.mjs index 3e3798c2..2e2c942e 100644 --- a/tests/org-sync-policy.test.mjs +++ b/tests/org-sync-policy.test.mjs @@ -42,7 +42,8 @@ test('organization credential names match destination org expectations without m for (const entry of [...policy.variables, ...policy.secrets]) { for (const field of ['visibility', 'repository_names', 'missing_repositories']) assert.equal(entry[field], undefined); assert.ok(!entry.name.startsWith('GITHUB_')); - assert.equal(entry.organizations.length, 1); + assert.ok(entry.organizations.length > 0); + assert.deepEqual(entry.organizations, [...new Set(entry.organizations)].toSorted(compareText)); assert.ok(entry.organizations.every(org => destinations.includes(org))); } const actual = [...policy.variables.map(entry => ({...entry, kind: 'variable'})), ...policy.secrets] From da724603de182dca23c04ac942b6a7189987c461 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 7 Nov 2026 11:20:58 -0500 Subject: [PATCH 22/22] ci: pin org sync to v2026.1107.144430 --- .github/org-sync.md | 2 +- .github/workflows/__global-replicator.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/org-sync.md b/.github/org-sync.md index 9c21f212..7369af20 100644 --- a/.github/org-sync.md +++ b/.github/org-sync.md @@ -67,7 +67,7 @@ from the policy does not delete it from a destination. `GH_BOT_TOKEN`. It must administer the destinations and read the source. Classic tokens need `admin:org`; webhook management also needs `admin:org_hook`. File delivery needs repository, workflow, and pull-request write access. GitHub App tokens need the API-specific organization permissions listed in the -[action guide](https://github.com/LizardByte/actions/blob/c18db340531d8d60a9083435cd65c4dd62e60956/actions/org_sync/administration.md). +[action guide](https://github.com/LizardByte/actions/blob/2dfa92c5136a2c66c5614ec766cc679f6f537334/actions/org_sync/administration.md). `ORG_SYNC_ADMIN_TOKEN` is controller-only and is not replicated. ## Discord webhooks diff --git a/.github/workflows/__global-replicator.yml b/.github/workflows/__global-replicator.yml index 80953bd9..78dcb2d0 100644 --- a/.github/workflows/__global-replicator.yml +++ b/.github/workflows/__global-replicator.yml @@ -47,7 +47,7 @@ jobs: - name: Synchronize organizations and shared files id: sync - uses: LizardByte/actions/actions/org_sync@c18db340531d8d60a9083435cd65c4dd62e60956 + uses: LizardByte/actions/actions/org_sync@2dfa92c5136a2c66c5614ec766cc679f6f537334 # v2026.1107.144430 env: APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }} APPLE_DEVELOPER_ID_APPLICATION_CERTIFICATE_BASE64: >-