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

42 阅读1分钟

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

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

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

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

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;

第二步,申请上传权限

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

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,如下:

<?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来进行签名。

// 第二步拿到的上传结果
const result = uploadApplyResult;

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

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

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

// 第二步拿到的结果
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。