ci: run CI only on Forgejo and delete the never-run GitHub workflows (WS-D)
The repository has Forgejo and GitLab remotes but no GitHub remote, so nothing under .github/workflows ever ran - including the daily Payple renewal job, which means Payple subscriptions were not being renewed. - Port payple-renew (daily cron + manual dispatch) to Forgejo. Requires the Forgejo secrets SUPABASE_URL and CRON_SECRET. - Port the CI checks (quality incl. version:check and contract:check, build, mobile quality, edge functions, API tests) to .forgejo/workflows/ci.yml. macOS and Android emulator jobs are dropped: no macOS runner here, and the signed Android release stays on GitLab mobile-production-release. - Keep one site deploy workflow (Linux, Cloudflare Pages); remove the manual Windows duplicate. The mobile release boundary self-test runs there. - Release verifiers read the Forgejo/GitLab workflows, fail if .github workflows come back, and check the rewritten site offers no mobile package. Policy: docs/REFACTOR_POLICY.md Wave 3, W3-7 and W3-8.
This commit is contained in:
parent
cd9d199dbf
commit
dc43884e3e
13 changed files with 269 additions and 1370 deletions
|
|
@ -1,6 +1,5 @@
|
|||
import { generateKeyPairSync } from 'node:crypto'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { createRequire } from 'node:module'
|
||||
import {
|
||||
existsSync,
|
||||
linkSync,
|
||||
|
|
@ -32,8 +31,6 @@ import {
|
|||
} from './mobile-release-evidence-lib.mjs'
|
||||
|
||||
const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..')
|
||||
const require = createRequire(import.meta.url)
|
||||
const { assertNoMobileArtifacts } = require('../deploy-site-to-nas.js')
|
||||
const productionAdMobId = 'ca-app-pub-1234567890123456~1234567890'
|
||||
const productionSigner = '4fac6924821c50daabed764932a53c486f8c6c5f34b9f18db920aa4099152b54'
|
||||
const versionName = '9.8.7'
|
||||
|
|
@ -101,15 +98,6 @@ function listScripts(directory) {
|
|||
}
|
||||
|
||||
function verifySourceContracts() {
|
||||
const legacySync = readWorkspaceFile('scripts/ci/sync-and-publish-forgejo-release.mjs')
|
||||
for (const forbidden of [
|
||||
'app-debug.apk',
|
||||
'd3ro-voice-v1.0.0.apk',
|
||||
'Android Release APK',
|
||||
]) {
|
||||
assert(!legacySync.includes(forbidden), `legacy_sync_contains_${forbidden.replace(/[^a-z0-9]+/gi, '_')}`)
|
||||
}
|
||||
|
||||
const retiredCapture = readWorkspaceFile('scripts/capture-pricing.js')
|
||||
for (const forbidden of [
|
||||
'assembleDebug',
|
||||
|
|
@ -125,71 +113,6 @@ function verifySourceContracts() {
|
|||
}
|
||||
assert(retiredCapture.includes('LEGACY_MOBILE_RELEASE_PIPELINE_DISABLED'), 'retired_capture_marker_missing')
|
||||
|
||||
const releaseWorkflow = readWorkspaceFile('.github/workflows/release.yml')
|
||||
for (const required of [
|
||||
'create-mobile-release-evidence.mjs',
|
||||
'prepare-mobile-release-publication.mjs',
|
||||
'ANDROID_RELEASE_EVIDENCE_PRIVATE_KEY_B64',
|
||||
'ANDROID_UPLOAD_CERT_SHA256',
|
||||
'release/android-release-identity.json',
|
||||
'release/mobile-release-evidence-public.pem',
|
||||
'--expected-admob-app-id',
|
||||
'--expected-upload-cert-sha256',
|
||||
'--repository "$GITHUB_REPOSITORY"',
|
||||
'--commit-sha "$GITHUB_SHA"',
|
||||
'--tree-sha "$(git rev-parse',
|
||||
'--git-ref "$GITHUB_REF"',
|
||||
'--workflow-identity "$GITHUB_WORKFLOW_REF"',
|
||||
'--run-id "$GITHUB_RUN_ID"',
|
||||
'--run-attempt "$GITHUB_RUN_ATTEMPT"',
|
||||
'--runner-identity "$RUNNER_NAME:$RUNNER_OS:$RUNNER_ARCH"',
|
||||
'test "$GITHUB_SHA" = "$(git rev-parse HEAD)"',
|
||||
'git status --porcelain --untracked-files=all',
|
||||
'--expected-play-app-signing-cert-sha256',
|
||||
'--forbidden-upload-cert-sha256',
|
||||
'--expected-version-name',
|
||||
'--expected-version-code',
|
||||
'--bundletool',
|
||||
'--snapshot-dir',
|
||||
'a099cfa1543f55593bc2ed16a70a7c67fe54b1747bb7301f37fdfd6d91028e29',
|
||||
'environment: mobile-production-release',
|
||||
'fetch-depth: 0',
|
||||
'git merge-base --is-ancestor',
|
||||
'tag_name: ${{ steps.release-identity.outputs.tag }}',
|
||||
'overwrite_files: false',
|
||||
'name: android-play-console-handoff',
|
||||
'REPOSITORY_VISIBILITY: ${{ github.event.repository.visibility }}',
|
||||
'test "$REPOSITORY_VISIBILITY" = "private"',
|
||||
'release-publication/app-release.aab',
|
||||
'release-publication/android-release-evidence.json',
|
||||
'release-publication/android-publication-manifest.json',
|
||||
'release-snapshot/release-artifact-verification.json',
|
||||
'retention-days: 7',
|
||||
]) {
|
||||
assert(releaseWorkflow.includes(required), `release_workflow_missing_${required.replace(/[^a-z0-9]+/gi, '_')}`)
|
||||
}
|
||||
for (const forbidden of [
|
||||
'ANDROID_RELEASE_CERT_SHA256',
|
||||
'--expected-cert-sha256',
|
||||
'ANDROID_RELEASE_EVIDENCE_PUBLIC_KEY_B64',
|
||||
'name: android-release-assets',
|
||||
'path: android-source/',
|
||||
'release-dist/android',
|
||||
]) {
|
||||
assert(!releaseWorkflow.includes(forbidden), `release_workflow_forbidden_${forbidden.replace(/[^a-z0-9]+/gi, '_')}`)
|
||||
}
|
||||
const handoffStep = releaseWorkflow.match(
|
||||
/- name: Upload Restricted Play Console AAB Handoff[\s\S]*?(?=\n\s{6}- name:|\n\s{2}#)/,
|
||||
)?.[0] ?? ''
|
||||
assert(handoffStep.includes('app-release.aab'), 'restricted_handoff_aab_missing')
|
||||
assert(!handoffStep.includes('app-release.apk'), 'restricted_handoff_contains_upload_key_apk')
|
||||
const publicReleaseStep = releaseWorkflow.slice(releaseWorkflow.indexOf('- name: Create GitHub Release'))
|
||||
assert(!publicReleaseStep.includes('.apk'), 'public_release_contains_android_apk')
|
||||
assert(!publicReleaseStep.includes('.aab'), 'public_release_contains_android_aab')
|
||||
assert(!publicReleaseStep.includes('android-play-console-handoff'), 'public_release_contains_android_handoff')
|
||||
assert(releaseWorkflow.includes('test "$GITHUB_SHA" = "$(git rev-parse origin/main)"'), 'dispatch_main_sha_guard_missing')
|
||||
assert(releaseWorkflow.includes('test "$RELEASE_TAG" = "v$VERSION_NAME"'), 'release_tag_identity_guard_missing')
|
||||
|
||||
const gitlabWorkflow = readWorkspaceFile('.gitlab-ci.yml')
|
||||
const mobileProductionJobStart = gitlabWorkflow.indexOf('mobile-production-release:\n')
|
||||
const publishReleaseJobStart = gitlabWorkflow.indexOf('\npublish-release:\n')
|
||||
|
|
@ -361,7 +284,6 @@ function verifySourceContracts() {
|
|||
for (const safePublicPath of [
|
||||
'site/public/.well-known/assetlinks.json',
|
||||
'site/public/accept-invite.css',
|
||||
'site/public/accept-invite.html',
|
||||
'site/public/accept-invite.js',
|
||||
'site/public/accept-invite/index.html',
|
||||
'site/public/download.html',
|
||||
|
|
@ -370,26 +292,25 @@ function verifySourceContracts() {
|
|||
const safePublicSource = readWorkspaceFile(safePublicPath)
|
||||
assert(!/(?:d3ro-voice[^"']*\.apk|git\.chanpaca\.net\/attachments\/(?:0b015367-dd8b-488c-8cc0-4db413b51792|d2e1b123-5678-496a-bf74-bc188938c999))/i.test(safePublicSource), `safe_public_mobile_reference_${safePublicPath}`)
|
||||
}
|
||||
const siteDeployWorkflow = readWorkspaceFile('.github/workflows/deploy-site.yml')
|
||||
const siteDeployWorkflow = readWorkspaceFile('.forgejo/workflows/deploy-site.yml')
|
||||
assert(siteDeployWorkflow.includes('verify-mobile-release-boundary.mjs --self-test'), 'pages_deploy_boundary_gate_missing')
|
||||
const deploySite = readWorkspaceFile('scripts/deploy-site-to-nas.js')
|
||||
assert(deploySite.includes('assertNoMobileArtifacts'), 'nas_mobile_artifact_guard_missing')
|
||||
assert(!deploySite.includes('sync-and-publish-forgejo-release'), 'nas_legacy_release_sync_enabled')
|
||||
const apiProject = readWorkspaceFile('apps/api-server/D3ROVoice.Api.csproj')
|
||||
assert(apiProject.includes('<Content Remove="wwwroot\\releases\\**\\*" />'), 'api_static_release_exclusion_missing')
|
||||
for (const staleAsset of ['index-D7M5UQvT.js', 'index-JlYFxlAJ.js']) {
|
||||
assert(apiProject.includes(`<Content Remove="wwwroot\\assets\\${staleAsset}" />`), `api_stale_marketing_asset_publishable_${staleAsset}`)
|
||||
}
|
||||
// Installers ship only through the Forgejo feed and public pages only from site/,
|
||||
// so the API must not serve static files at all.
|
||||
const apiProgram = readWorkspaceFile('apps/api-server/Program.cs')
|
||||
assert(apiProgram.includes('mobileReleasePath') && apiProgram.indexOf('mobileReleasePath') < apiProgram.indexOf('app.UseStaticFiles()'), 'api_runtime_mobile_release_guard_missing')
|
||||
assert(apiProgram.includes('legacyMarketingAsset'), 'api_runtime_legacy_marketing_guard_missing')
|
||||
const osHook = readWorkspaceFile('site/src/hooks/useClientOS.ts')
|
||||
const androidConfig = osHook.match(/android:\s*\{[\s\S]*?\n\s*\},/)?.[0] ?? ''
|
||||
assert(androidConfig.includes("downloadUrl: '#download'"), 'android_download_not_unavailable')
|
||||
assert(!androidConfig.includes('attachments/'), 'android_attachment_link_enabled')
|
||||
const downloadUi = readWorkspaceFile('site/src/sections/Download.tsx')
|
||||
assert(downloadUi.includes('aria-disabled="true"'), 'android_download_ui_not_disabled')
|
||||
assert(!/attachments\/[0-9a-f-]+[\s\S]{0,120}\.apk/i.test(downloadUi), 'android_direct_attachment_enabled')
|
||||
assert(!apiProgram.includes('UseStaticFiles'), 'api_serves_static_files')
|
||||
// 사이트는 Android·iOS 설치 파일을 내려주지 않는다. 다운로드 버튼은 Windows 설치 파일 하나뿐이고
|
||||
// 모바일은 '준비 중' 문구로만 안내한다(site/src/sections/Download.tsx, Hero.tsx).
|
||||
for (const sitePath of [
|
||||
'site/src/hooks/useClientOS.ts',
|
||||
'site/src/sections/Hero.tsx',
|
||||
'site/src/sections/Download.tsx',
|
||||
'site/src/release.ts',
|
||||
]) {
|
||||
const siteSource = readWorkspaceFile(sitePath)
|
||||
assert(!/\.(?:apk|aab)\b/i.test(siteSource), `site_mobile_package_link_${sitePath}`)
|
||||
assert(!/attachments\/[0-9a-f-]+/i.test(siteSource), `site_attachment_link_${sitePath}`)
|
||||
assert(!/play\.google\.com\/store|apps\.apple\.com/i.test(siteSource), `site_store_link_${sitePath}`)
|
||||
}
|
||||
}
|
||||
|
||||
function expectFailure(label, operation, expectedCode) {
|
||||
|
|
@ -800,14 +721,6 @@ function verifyNegativeAndMaterializationTests() {
|
|||
expectFailure('hardlink_source', () => hashRegularFileStable(hardlinkPath), 'hardlink_rejected')
|
||||
rmSync(hardlinkPath)
|
||||
|
||||
const unsafeSite = join(temporaryRoot, 'unsafe-site')
|
||||
mkdirSync(unsafeSite)
|
||||
writeFileSync(join(unsafeSite, 'legacy.js'), 'location.href="https://git.chanpaca.net/attachments/0b015367-dd8b-488c-8cc0-4db413b51792"')
|
||||
expectFailure('static_link_bypass', () => assertNoMobileArtifacts(unsafeSite), 'Legacy mobile download link blocked')
|
||||
rmSync(join(unsafeSite, 'legacy.js'))
|
||||
writeFileSync(join(unsafeSite, 'unsealed.apk'), 'not a release')
|
||||
expectFailure('static_apk_bypass', () => assertNoMobileArtifacts(unsafeSite), 'Unsealed mobile artifact blocked')
|
||||
|
||||
const outsideRoot = join(temporaryRoot, 'outside')
|
||||
mkdirSync(outsideRoot)
|
||||
const outsideApk = join(outsideRoot, RELEASE_APK_NAME)
|
||||
|
|
|
|||
|
|
@ -33,11 +33,6 @@ function loadSurfaces(readSurface = read) {
|
|||
publisher: readSurface('scripts/ci/publish-gitlab-release.mjs'),
|
||||
forgejoPublisher: readSurface('scripts/ci/publish-forgejo-release.mjs'),
|
||||
gitlab: readSurface('.gitlab-ci.yml'),
|
||||
github: readSurface('.github/workflows/release.yml'),
|
||||
githubMac: readSurface('.github/workflows/build-mac.yml'),
|
||||
githubSigning: readSurface('.github/workflows/release-signing-ca.yml'),
|
||||
forgejoLinux: readSurface('.forgejo/workflows/deploy-site.yml'),
|
||||
forgejoWindows: readSurface('.forgejo/workflows/deploy-site-windows.yml'),
|
||||
forgejoRelease: readSurface('.forgejo/workflows/release.yml'),
|
||||
changelog: readSurface('CHANGELOG.md'),
|
||||
}
|
||||
|
|
@ -208,7 +203,7 @@ function validate(surfaces) {
|
|||
|
||||
for (const [name, workflow] of [
|
||||
['gitlab', surfaces.gitlab],
|
||||
['github', surfaces.github],
|
||||
['forgejo', surfaces.forgejoRelease],
|
||||
]) {
|
||||
fail(workflow.includes('sync-version.mjs'), `${name}_version_gate_missing`)
|
||||
fail(workflow.includes('release/product-version.json'), `${name}_product_metadata_missing`)
|
||||
|
|
@ -221,10 +216,8 @@ function validate(surfaces) {
|
|||
fail(/tags:/.test(surfaces.forgejoRelease), 'forgejo_release_workflow_tag_trigger_missing')
|
||||
fail(surfaces.forgejoRelease.includes('sync-version.mjs'), 'forgejo_release_workflow_version_gate_missing')
|
||||
|
||||
fail(!/push:\s*\n\s*tags:/m.test(surfaces.githubMac), 'legacy_mac_tag_trigger_enabled')
|
||||
fail(!/push:\s*\n\s*tags:/m.test(surfaces.githubSigning), 'legacy_signing_tag_trigger_enabled')
|
||||
fail(!surfaces.forgejoLinux.includes('sync-and-publish-forgejo-release'), 'forgejo_linux_legacy_release_sync')
|
||||
fail(!surfaces.forgejoWindows.includes('sync-and-publish-forgejo-release'), 'forgejo_windows_legacy_release_sync')
|
||||
// CI 정본은 Forgejo 한 벌이다(REFACTOR_POLICY W3-7). GitHub 원격이 없어 .github 워크플로는 실행되지 않는다.
|
||||
fail(!existsSync(join(root, '.github/workflows')), 'github_workflows_reintroduced')
|
||||
fail(!existsSync(join(root, 'apps/mobile-rn/src/lib/update-manager.ts')), 'unsafe_mobile_update_manager_present')
|
||||
fail(
|
||||
existsSync(join(root, `apps/mobile-rn/metadata/android/ko-KR/changelogs/${metadata.androidVersionCode}.txt`)),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue