记录CI/CD自动化上传AppGallery遇到的坑

本文主要声讨的是华为 AppGallery 开发者文档。要值得称赞的是,他们还是写了为了这个功能编写了文档。如果你在写CI/CD的时候遇到这个问题,并通过该文章解决了,麻烦点个赞。

一般来说,你需要按照以下几步走。

第一步,获取华为应用市场 Access Token

首先,一开始,先看文档。获取服务端授权,然后把拿到的client_idclient_secret 进行如下请求。

typescript 复制代码
const result = axios.post('https://connect-api.cloud.huawei.com/api/oauth2/v1/token', {
  grant_type: 'client_credentials',
  client_id: clientId, // 替换为你的 client_id
  client_secret: clientSecret, // 替换为你的 client_secret
});

// 拿到的 Token
const accessToken = result.access_token;

第二步,申请上传权限

接着,直接看代码就行,到这一步还没有坑。

typescript 复制代码
const contentLength = fs.statSync(apkFilePath).size;
  
const uploadApplyResult = axios.get(
  "https://connect-api.cloud.huawei.com/api/publish/v2/upload-url/for-obs", 
  {
    headers: {
      client_id: clientId,
      // 第一步拿到的 token
      Authorization: `Bearer ${accessToken}`,
      "Content-Type": "application/json",
    },
    
    params: {
      // 根据华为 API 要求构造请求体

      // 1 代表 APK, 2 代表 AAB(Android App Bundle)
      type: 1,
      // 你在华为平台的应用 ID
      appId: appId,
      // 'apk' or 'aab'
      suffix: 'apk',

      // 上传的 APK 文件名
      fileName: fileName,
      
      // 上传的 APK 文件大小
      contentLength: contentLength,

      // 大陆地区可以忽略,海外地区必填
      chineseMainlandFlag: 0,
    },
  }
)

第三部,开始上传 APK 文件

我们要求是只需要上传到平台就行,所以不用搞分片上传。这一步就是坑的开始,如果你按照文档来,百分百会给你报一串错误 XML,如下:

txt 复制代码
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Error>
  <Code>SignatureDoesNotMatch</Code>
  <Message>The request signature we calculated does not match the signature you provided. Check your
    key and signing method.</Message>
  <RequestId>0000019AED024D5C94169C0FA01D180A</RequestId>
  <HostId>Z9v+cC1sRnaWw6x0vi8pxxYA0YVnKxbYHUPAFpnxkX8sLV44u5b02Z+ailn2wCnR</HostId>
  <AWSAccessKeyId>NT1DSSQ7R3FSAEMCBELN</AWSAccessKeyId>
  <SignatureProvided>fbd22dc7ed5bf90a420fcd6bd78b3fae93e68f2668758c53cbb13c7555913422</SignatureProvided>
  <StringToSign>AWS4-HMAC-SHA256
    20251205T053539Z
    20251205/ap-southeast-3/s3/aws4_request
    0b4bc19ab88df2d905cfbba6f3f24fd722da7cb7434fc9d2288776eaed7cd15f</StringToSign>
  <StringToSignBytes>41 57 53 34 2d 48 4d 41 43 2d 53 48 41 32 35 36 0a 32 30 32 35 31 32 30 35 54
    30 35 33 35 33 39 5a 0a 32 30 32 35 31 32 30 35 2f 61 70 2d 73 6f 75 74 68 65 61 73 74 2d 33 2f
    73 33 2f 61 77 73 34 5f 72 65 71 75 65 73 74 0a 30 62 34 62 63 31 39 61 62 38 38 64 66 32 64 39
    30 35 63 66 62 62 61 36 66 33 66 32 34 66 64 37 32 32 64 61 37 63 62 37 34 33 34 66 63 39 64 32
    32 38 38 37 37 36 65 61 65 64 37 63 64 31 35 66</StringToSignBytes>
  <CanonicalRequest>PUT
    /SG/2025120505/1764912939540-2257a16f-c80f-4fe3-bdde-f3150531d3c4.apk
    content-type:application/octet-stream
    host:nsp-appgallery-agcfs-dra.obs.ap-southeast-3.myhuaweicloud.com
    x-amz-content-sha256:UNSIGNED-PAYLOAD
    x-amz-date:20251205T053539Z
    ontent-length;content-type;host;x-amz-content-sha256;x-amz-date
    UNSIGNED-PAYLOAD
    </CanonicalRequest>
  <StringToSignBytes>50 55 54 0a 2f 53 47 2f 32 30 32 35 31 32 30 35 30 35 2f 31 37 36 34 39 31 32
    39 33 39 35 34 30 2d 32 32 35 37 61 31 36 66 2d 63 38 30 66 2d 34 66 65 33 2d 62 64 64 65 2d 66
    33 31 35 30 35 33 31 64 33 63 34 2e 61 70 6b 0a 0a 63 6f 6e 74 65 6e 74 2d 74 79 70 65 3a 61 70
    70 6c 69 63 61 74 69 6f 6e 2f 6f 63 74 65 74 2d 73 74 72 65 61 6d 0a 68 6f 73 74 3a 6e 73 70 2d
    61 70 70 67 61 6c 6c 65 72 79 2d 61 67 63 66 73 2d 64 72 61 2e 6f 62 73 2e 61 70 2d 73 6f 75 74
    68 65 61 73 74 2d 33 2e 6d 79 68 75 61 77 65 69 63 6c 6f 75 64 2e 63 6f 6d 0a 78 2d 61 6d 7a 2d
    63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 3a 55 4e 53 49 47 4e 45 44 2d 50 41 59 4c 4f 41 44 0a
    78 2d 61 6d 7a 2d 64 61 74 65 3a 32 30 32 35 31 32 30 35 54 30 35 33 35 33 39 5a 0a 0a 63 6f 6e
    74 65 6e 74 2d 6c 65 6e 67 74 68 3b 63 6f 6e 74 65 6e 74 2d 74 79 70 65 3b 68 6f 73 74 3b 78 2d
    61 6d 7a 2d 63 6f 6e 74 65 6e 74 2d 73 68 61 32 35 36 3b 78 2d 61 6d 7a 2d 64 61 74 65 0a 55 4e
    53 49 47 4e 45 44 2d 50 41 59 4c 4f 41 44</StringToSignBytes>
</Error>

其实问题的症结就在于,文档没告诉你,你需要把Content-Length放入头部,他的头部签名字段x-amz-content-sha256中需要Content-Length来进行签名。

typescript 复制代码
// 第二步拿到的上传结果
const result = uploadApplyResult;

// 文件流
const fileStream = fs.createReadStream(apkFilePath);

// 现在不会报错了...
const response = await axios.put(result.urlInfo.url, fileStream, {
  headers: {
    ...result.urlInfo.headers,
    'Content-Length': contentLength
  }
});

第四步,更新应用文件信息

typescript 复制代码
// 第二步拿到的结果
const objectId = uploadApplyResult.urlInfo.objectId;

const fileName = 'my.apk'

const fileInfoResult = await httpClient.put(
  `https://connect-api.cloud.huawei.com/api/publish/v2/app-file-info?appId=${appId}`,
  {
    // apk: 5, aab: 6
    'fileType': 5,
    'files': [{
      'fileName': fileName,
      'fileDestUrl': result.urlInfo.objectId
    }]
  },
);

最后

我看到截止目前 2025-12-25 为止,AppGallery 已经从 v2 升级到了 v3。所以,搞 API 没啥前途。你看到上面所谓的 SignatureDoesNotMatch 后,能第一时间想到看是不是少了或者多了参数,这种解决问题的直觉才是我认为最重要的。

ps: 英文版文档还没改,用的还是 v2 的 API。

相关推荐
Yanni4Night2 小时前
使用URLPattern API构建自己的路由器 🛣️
前端·javascript
web守墓人2 小时前
【前端】garn:使用go实现一款类似yarn的依赖管理器
前端
全栈陈序员2 小时前
Vue 实例挂载的过程是怎样的?
前端·javascript·vue.js·学习·前端框架
WordPress学习笔记3 小时前
wordpress根据页面别名获取该页面的链接
android·wordpress
Bruce_Liuxiaowei3 小时前
一键清理Chrome浏览器缓存:批处理与PowerShell双脚本实现
前端·chrome·缓存
怒放的生命19913 小时前
Vue 2 vs Vue 3对比 编译原理不同深度解析
前端·javascript·vue.js
2501_916007473 小时前
iOS 崩溃日志的分析方法,将崩溃日志与运行过程结合分析
android·ios·小程序·https·uni-app·iphone·webview
GDAL3 小时前
html返回顶部实现方式对比
前端·html·返回顶部
Violet_YSWY3 小时前
ES6 () => ({}) 语法解释
前端·ecmascript·es6