upload-target.ts 5.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. /** Upload one validated Desktop release to its Tencent COS update directory. */
  2. import { resolve } from 'node:path'
  3. import { parseArgs } from 'node:util'
  4. import type { DesktopPackageTargetName } from './package-target.ts'
  5. import { createDesktopCos } from './desktop-cos.ts'
  6. import { resolveDesktopUploadConfig } from './desktop-auto-update-environment.mjs'
  7. import { loadDesktopPackageEnvironment } from './desktop-package-environment.mjs'
  8. import { createDesktopUploadPlan } from './desktop-upload-plan.ts'
  9. import { uploadDesktopRelease } from './desktop-upload-run.ts'
  10. const SUPPORTED_TARGETS = new Set<DesktopPackageTargetName>(['mac-arm64', 'mac-x64', 'win-x64'])
  11. function targetName(value: string): DesktopPackageTargetName {
  12. if (!SUPPORTED_TARGETS.has(value as DesktopPackageTargetName)) {
  13. throw new Error(`desktop upload: unsupported target ${JSON.stringify(value)}; expected ${[...SUPPORTED_TARGETS].join(', ')}`)
  14. }
  15. return value as DesktopPackageTargetName
  16. }
  17. function requiredEnvironmentValue(environment: NodeJS.ProcessEnv, name: string): string {
  18. const value = environment[name]?.trim()
  19. if (value === undefined || value === '') {
  20. throw new Error(`desktop upload: ${name} must be set to a non-empty value`)
  21. }
  22. return value
  23. }
  24. /**
  25. * Use the credential launcher's selected deployment only when it matches the packaged release destination.
  26. * @param fileEnvironment Target dotenv settings that own the release destination.
  27. * @param injectedEnvironment Child environment containing the DPAPI-decrypted credential pair.
  28. * @param selected Deployment authorized by the launcher operator.
  29. * @param bucket Bucket authorized by the launcher operator.
  30. * @param target Packaged target being uploaded.
  31. * @returns Release settings with only the selected credential pair replaced.
  32. */
  33. export function resolveCredentialUploadEnvironment(
  34. fileEnvironment: NodeJS.ProcessEnv, injectedEnvironment: NodeJS.ProcessEnv,
  35. selected: 'test' | 'production', bucket: string,
  36. target: DesktopPackageTargetName,
  37. ): NodeJS.ProcessEnv {
  38. const platform = target === 'win-x64' ? 'win32' : 'darwin'
  39. const arch = target === 'mac-arm64' ? 'arm64' : 'x64'
  40. const destination = resolveDesktopUploadConfig(fileEnvironment, platform, arch)
  41. if (destination.environment !== selected || destination.bucket !== bucket) {
  42. throw new Error('desktop upload: credential launcher deployment or bucket differs from the packaged release destination')
  43. }
  44. if (injectedEnvironment.DSH_DESKTOP_AUTO_UPDATE_ENV !== selected
  45. || injectedEnvironment[`${selected === 'test' ? 'DOWNLOAD_TEST' : 'DOWNLOAD_PROD'}_COS_BUCKET`] !== bucket) {
  46. throw new Error('desktop upload: credential launcher environment differs from its explicit arguments')
  47. }
  48. return {
  49. ...fileEnvironment,
  50. [destination.secretIdEnvName]: requiredEnvironmentValue(injectedEnvironment, destination.secretIdEnvName),
  51. [destination.secretKeyEnvName]: requiredEnvironmentValue(injectedEnvironment, destination.secretKeyEnvName),
  52. }
  53. }
  54. async function main(): Promise<void> {
  55. const { positionals, values } = parseArgs({ args: process.argv.slice(2), allowPositionals: true, options: {
  56. 'credential-launcher': { type: 'boolean' }, environment: { type: 'string' }, bucket: { type: 'string' },
  57. } })
  58. const target = positionals[0]
  59. if (target === undefined || positionals.length !== 1) {
  60. throw new Error('desktop upload: expected exactly one target')
  61. }
  62. const name = targetName(target)
  63. const fileEnvironment = loadDesktopPackageEnvironment(name === 'win-x64' ? 'win32' : 'darwin')
  64. const launcher = values['credential-launcher'] === true
  65. if (launcher ? values.environment === undefined || values.bucket === undefined
  66. : values.environment !== undefined || values.bucket !== undefined) {
  67. throw new Error('desktop upload: credential launcher requires an explicit environment and bucket')
  68. }
  69. if (values.environment !== undefined && values.environment !== 'test' && values.environment !== 'production') {
  70. throw new Error('desktop upload: credential launcher environment must be test or production')
  71. }
  72. const environment = launcher
  73. ? resolveCredentialUploadEnvironment(fileEnvironment, process.env, values.environment as 'test' | 'production', values.bucket!, name)
  74. : fileEnvironment
  75. const plan = await createDesktopUploadPlan(name, { environment })
  76. const cos = createDesktopCos({
  77. secretId: requiredEnvironmentValue(environment, plan.secretIdEnvName),
  78. secretKey: requiredEnvironmentValue(environment, plan.secretKeyEnvName),
  79. })
  80. process.stdout.write(`desktop upload: ${plan.target} ${plan.version} -> ${plan.publicUrl}\n`)
  81. await uploadDesktopRelease(plan, cos, resolve(import.meta.dirname, '../.desktop-build/upload-records'))
  82. }
  83. if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) {
  84. main().catch(() => {
  85. process.stderr.write('desktop upload: failed; inspect the printed record directory if allocated. No automatic retry; reconcile remote state before another upload.\n')
  86. process.exitCode = 1
  87. })
  88. }