mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
* feat(apps): add design_html app type support and credential author identity - Add design_html to appTypePolicies (same as modern_html: skip install/env-pull/skills-sync) - Route +html-publish via policy (useTOSPublish) instead of hardcoded type check - Parse commit_author_name/commit_author_email from +git-credential-init response - Use server-provided author identity for repo-local git config, fallback to defaults - Support meta_token as identifier in +get command - Use envvars.AgentName() for source_agent in +create (reads LARKSUITE_CLI_AGENT_NAME) - Add creative HTML guide reference skeleton and SKILL.md routing entry - Update git-credential skill docs with new output fields * fix(apps): unify html-publish to TOS path, add html to init skip policy - Remove useTOSPublish policy field, html-publish always uses TOS upload - Add html type to appTypePolicies (skip install/env-pull/skills-sync) - Remove design_html from policies (not yet in use) - Fix git credential dry-run test for new local_effects entry * feat(apps): validate --app-id format to reject meta_token with resolution hint * feat(apps): integrate creative-design skill and update skill docs - Add creative-design skill under lark-apps/ (same level as references/) - Update SKILL.md description with creative design trigger keywords - Add creative design routing in development path selection table - Add --path relative path guidance in html-publish reference - Remove old creative-html-guide skeleton (replaced by creative-design) * feat(apps): skip app sync for html/modern_html in +init Add skipAppSync policy field; html and modern_html skip npx app sync on non-empty repo path since static HTML sites don't need it. * fix(apps): merge creative-design into html routing and add intent entry - Merge static HTML and creative-design into one path selection row - Add creative-design intent routing entry before html-publish * docs(apps): add html local dev flow, unify publish link source - Add html端到端 flow in local-dev.md (create → init → dev → release-create) - Unify publish link source: html and full_stack both use +release-get - Update SKILL.md routing and publish护栏 accordingly * fix(apps): update html-publish dry-run and skill docs for TOS flow - DryRun shows actual 3-step TOS flow (pre_release → TOS PUT → release-create) - Skill docs: output is release_id, use +release-get to poll for online_url - Remove references to legacy multipart upload and data.url * TEMP: pin miaoda-cli alpha and add BOE header for testing - Pin miaoda-cli to 0.1.24-alpha.fb2cf0a (revert to @latest before merge) - Add x-tt-env=boe_aily_lark_cli header globally (remove before merge) - html app-type uses --template design-html instead of --app-type (remove before merge) * docs(apps): add creative mode link format and meta_token recognition - Add creative mode (html) link format `https://{tenant}/page/{meta_token}` in publish护栏 - Note dev and publish URLs are the same for creative mode, unlike full_stack - Add meta_token to app_id resolution with full link format in app_id获取 * docs(apps): route html apps through local-dev git pipeline by default - Select dev path: html apps now default to local-dev pipeline instead of skipping local/cloud axis - Intent routing: creative-design publishes via local-dev flow instead of +html-publish - Remove +html-publish fallback from local-dev "when not to use" section * docs(apps): generalize skill references to cover both html and full_stack Remove full_stack-only wording from init, create, list, env-pull, and release-create references since html apps now share the same local dev and release flow. * feat(apps): add meta_token to +get pretty output and dry-run description * docs(apps): unify html as creative mode, fix routing and local-dev flow - Remove "HTML" as separate dev path; html and full_stack both go through local-dev - Intent routing: read local-dev before creative-design to establish git pipeline first - Mark +html-publish as legacy, redirect to local-dev for creative mode - Split html local-dev into 3 scenarios: first-time, iteration, pre-generated files - git add . instead of selective add to capture all creative-design output files * docs(apps): remove dev link from html-publish output, only return release-get online_url * fix: add license header to deck-stage.js * docs(apps): clarify dev link only for full_stack, creative mode shares dev/pub URL * docs(apps): remove +html-publish from intent routing, description, and guardrails All HTML apps now go through local-dev pipeline. +html-publish is deprecated. * docs(apps): remove html-publish references from create/release-create/cloud-dev pages html-publish is no longer the recommended path for HTML apps; all html and full_stack apps now follow the same local-dev + release-create flow. * fix(apps): address PR review feedback - html-publish dry-run: register all 3 API calls (GET pre_release, PUT TOS, POST release-create) instead of hiding steps in metadata - validateRealAppID: remove cli_ prefix check (not a valid app_id prefix) - E2E: update git-credential dry-run to expect 4 local_effects - E2E: update html-publish dry-run to expect GET pre_release * fix(apps): address PR review — remove legacy multipart dead code, fix docs - Delete html_publish_client.go and html_publish_client_test.go (legacy multipart) - Remove runHTMLPublish, enrichHTMLPublishAPIError, buildHTMLPublishFailureHint - Migrate tests from runHTMLPublish to prepareHTMLPublishTarball (same coverage) - Remove cli_ prefix from validateRealAppID (not a valid app_id prefix) - Fix html-publish.md error wording to match actual message - Register all 3 TOS API calls in html-publish dry-run - Update E2E tests for new dry-run contract * fix(apps): correctly merge SKILL.md with main (role mgmt, auth wording, source boundary) Rebuild SKILL.md from our branch version, then merge in main's additions: - description: add HTML静态站点发布, 应用角色与成员管理, 应用角色/角色成员 - 身份与授权: use main's updated wording (no proactive re-login) - intent routing: add +role-* row, +init refs 平台资源与应用源码边界 - 能力边界 → 平台资源与应用源码边界 (7 rules from main) - 禁止预授权底线: add role ② and html-publish ③ clauses * docs(apps): route legacy html-publish only for non-git html apps * docs(apps): strengthen local-dev routing and git recovery guidance fix:cherry-pick and resolve conflicts * fix: gofmt apps_errors.go and apps_errors_test.go * docs(apps): strengthen git credential recovery and add file-upload guidance - Generalize git error recovery: any git operation failure triggers +git-credential-init refresh, with environment analysis on failure - Add resource file upload rule: use +file-upload instead of local paths, base64 inlining, or git commits; files are app-scoped * test(apps): strengthen html-publish dry-run assertions for TOS 3-step contract * fix: 文件资源上传 * docs(apps): update creative-design skill content * fix: re-add license header to deck-stage.js * refactor(apps): merge system-prompt.md into SKILL.md for creative-design skill Consolidate the thin SKILL.md wrapper and the full system-prompt.md methodology into a single file, eliminating an unnecessary indirection. Update references in claude.md and codex.md accordingly. * chore: revert TEMP changes — miaoda-cli back to @latest, remove BOE header * docs(apps): remove 可见范围 from 发布态护栏 创意模式的可见范围权限走 lark-drive 文档权限体系,而非妙搭应用 权限体系,当前的 +access-scope-set/get 无法正确管理创意模式应用 的可见范围。待文档协作支持妙搭能力后,再通过 lark-drive 域能力 引导修改。 TODO: 等文档协作支持妙搭能力后,在 skill 中加入使用文档域权限 能力修改创意模式可见范围的引导。 * docs(lark-apps): 在平台资源与应用源码边界添加路径规则,引导 agent 使用相对路径 `apps` 命令的 `--path`、`--file`、`--output` 只接受 cwd 下的相对路径,传绝对路径会报错。 * docs(lark-apps): 新增创意模式评论路由和裸 meta_token 识别引导 - 意图路由表新增创意模式应用评论,引导走 lark-drive 文档评论体系 - app_id 获取章节补充裸 meta_token 识别:非链接非 app_ 开头时尝试用 +get 解析 * refactor(apps): flatten creative-design built-in-skills into references - Delete built-in-skills/ directory (9 nested sub-skill folders) - Move media skill content to references/ as flat .md files - Add assets/index.html React+Babel starter template - Integrate publishing flow into creative-design SKILL.md - Update harness reference docs (aily/claude/codex.md) - Simplify lark-apps SKILL.md routing to point directly to creative-design - Remove creative-design standalone .git directory * refactor(apps): rename creative-design/SKILL.md to creative-design.md Avoid being mistaken as an independent skill entry point. Update all internal references (lark-apps routing table + 10 reference files). * fix(apps): fail closed when queryAppType fails instead of falling back to full_stack queryAppType now returns an error instead of silently returning "". +init aborts if the app type cannot be determined, preventing wrong scaffold type from being committed and pushed to the repository. --------- Co-authored-by: zhangli <zhangli.268@bytedance.com>
333 lines
13 KiB
Go
333 lines
13 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package apps
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"github.com/larksuite/cli/errs"
|
||
"github.com/larksuite/cli/extension/fileio"
|
||
"github.com/larksuite/cli/internal/validate"
|
||
"github.com/larksuite/cli/shortcuts/common"
|
||
)
|
||
|
||
// AppsHTMLPublish packs --path as tar.gz and publishes an HTML app.
|
||
var AppsHTMLPublish = common.Shortcut{
|
||
Service: appsService,
|
||
Command: "+html-publish",
|
||
Description: "Publish HTML to an app (returns url or release_id depending on app type)",
|
||
Risk: "write",
|
||
Tips: []string{
|
||
"Example: lark-cli apps +html-publish --app-id <app_id> --path ./dist",
|
||
"Example: lark-cli apps +html-publish --app-id <app_id> --path ./site --dry-run",
|
||
},
|
||
Scopes: []string{"spark:app:write", "spark:app:read"},
|
||
AuthTypes: []string{"user"},
|
||
HasFormat: true,
|
||
Flags: []common.Flag{
|
||
{Name: "app-id", Desc: "app ID", Required: true},
|
||
{Name: "path", Desc: "path to HTML file or directory", Required: true},
|
||
{Name: "allow-sensitive", Type: "bool", Desc: "skip the credential-file scan (allow .env / .npmrc / .aws/credentials / etc. in the publish payload)"},
|
||
},
|
||
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
|
||
appID := strings.TrimSpace(rctx.Str("app-id"))
|
||
if appID == "" {
|
||
return appsValidationParamError("--app-id", "--app-id is required")
|
||
}
|
||
if err := validateRealAppID(appID); err != nil {
|
||
return err
|
||
}
|
||
path := strings.TrimSpace(rctx.Str("path"))
|
||
if path == "" {
|
||
return appsValidationParamError("--path", "--path is required")
|
||
}
|
||
// Block well-known credential files in the publish payload unless the
|
||
// caller explicitly opts in. Lives in Validate (not DryRun) so that
|
||
// `--dry-run` returns non-zero on hit — the framework runs Validate
|
||
// before branching to DryRun/Execute, so both paths share this gate.
|
||
if rctx.Bool("allow-sensitive") {
|
||
return nil
|
||
}
|
||
candidates, err := walkHTMLPublishCandidates(rctx.FileIO(), path)
|
||
if err != nil {
|
||
// Don't fail Validate on walk errors (bad --path, etc.) — let
|
||
// DryRun/Execute surface them in their own (richer) envelopes.
|
||
return nil
|
||
}
|
||
var hits []string
|
||
for _, c := range candidates {
|
||
if isSensitiveCandidate(path, c) {
|
||
hits = append(hits, c.RelPath)
|
||
}
|
||
}
|
||
if len(hits) > 0 {
|
||
return sensitiveCandidatesError(hits)
|
||
}
|
||
return nil
|
||
},
|
||
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
|
||
appID := strings.TrimSpace(rctx.Str("app-id"))
|
||
path := strings.TrimSpace(rctx.Str("path"))
|
||
dry := common.NewDryRunAPI()
|
||
dry.Desc("Pack tar.gz → GET pre_release for TOS upload URL → PUT tar.gz to TOS → POST release-create with tos_path; returns release_id")
|
||
dry.GET(fmt.Sprintf("%s/apps/%s/pre_release", apiBasePath, validate.EncodePathSegment(appID))).
|
||
PUT("<presigned_upload_url> (from pre_release response)").
|
||
POST(fmt.Sprintf(releaseCreatePath, validate.EncodePathSegment(appID))).
|
||
Body(map[string]string{"tos_path": "<from pre_release response>"})
|
||
|
||
candidates, err := walkHTMLPublishCandidates(rctx.FileIO(), path)
|
||
if err != nil {
|
||
dry.Set("path_error", err.Error())
|
||
return dry
|
||
}
|
||
if err := ensureIndexHTML(candidates); err != nil {
|
||
// Surface the same failure Execute would hit, but as a structured
|
||
// envelope field so dry-run still exits 0 (matches repo convention
|
||
// for dry-run "advisory preview" semantics).
|
||
dry.Set("validation_error", err.Error())
|
||
}
|
||
if hits := oversizeHTMLFiles(candidates); len(hits) > 0 {
|
||
dry.Set("oversize_html", hits)
|
||
}
|
||
dry.Set("file_count", len(candidates))
|
||
var totalSize int64
|
||
names := make([]string, 0, len(candidates))
|
||
for _, c := range candidates {
|
||
totalSize += c.Size
|
||
names = append(names, c.RelPath)
|
||
}
|
||
dry.Set("total_size_bytes", totalSize)
|
||
dry.Set("files", names)
|
||
// Sensitive-file rejection lives in Validate (so dry-run exits non-zero
|
||
// on hit). When --allow-sensitive is set, still surface the list here
|
||
// as an info field so the caller sees what was waived.
|
||
if rctx.Bool("allow-sensitive") {
|
||
var waived []string
|
||
for _, c := range candidates {
|
||
if isSensitiveCandidate(path, c) {
|
||
waived = append(waived, c.RelPath)
|
||
}
|
||
}
|
||
if len(waived) > 0 {
|
||
dry.Set("sensitive_waived", waived)
|
||
dry.Set("sensitive_waived_summary", fmt.Sprintf("%d credential file(s) included because --allow-sensitive is set", len(waived)))
|
||
}
|
||
}
|
||
return dry
|
||
},
|
||
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
|
||
spec := appsHTMLPublishSpec{
|
||
AppID: strings.TrimSpace(rctx.Str("app-id")),
|
||
Path: strings.TrimSpace(rctx.Str("path")),
|
||
}
|
||
|
||
out, err := runHTMLPublishTOS(ctx, rctx, spec)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
rctx.OutFormat(out, nil, func(w io.Writer) {
|
||
if url, ok := out["url"].(string); ok && url != "" {
|
||
fmt.Fprintf(w, "url: %s\n", url)
|
||
}
|
||
if rid, ok := out["release_id"].(string); ok && rid != "" {
|
||
fmt.Fprintf(w, "release_id: %s\n", rid)
|
||
}
|
||
})
|
||
return nil
|
||
},
|
||
}
|
||
|
||
type appsHTMLPublishSpec struct {
|
||
AppID string
|
||
Path string
|
||
}
|
||
|
||
// maxSensitiveListInError caps how many credential-file matches we list inline
|
||
// in the validation error, so the message stays readable when a misconfigured
|
||
// payload has many hits (e.g. a directory tree accidentally containing
|
||
// per-environment .env.* files for every stage).
|
||
const maxSensitiveListInError = 5
|
||
|
||
// truncatedJoin joins items with ", ", capping at max entries and appending
|
||
// "(and N more)" for the remainder, so an inline error list stays readable when
|
||
// a payload has many hits.
|
||
func truncatedJoin(items []string, max int) string {
|
||
if len(items) <= max {
|
||
return strings.Join(items, ", ")
|
||
}
|
||
return strings.Join(items[:max], ", ") + fmt.Sprintf(" (and %d more)", len(items)-max)
|
||
}
|
||
|
||
// sensitiveCandidatesError builds the Validate-time rejection when --path
|
||
// contains credential files and --allow-sensitive was not set.
|
||
func sensitiveCandidatesError(hits []string) error {
|
||
return appsValidationParamError("--path",
|
||
"--path contains %d credential file(s) that should not be published: %s",
|
||
len(hits), truncatedJoin(hits, maxSensitiveListInError)).
|
||
WithHint("remove these files from the publish payload, OR pass --allow-sensitive if shipping them is intentional (e.g. a docs site demoing credential-file formats)")
|
||
}
|
||
|
||
// maxHTMLPublishTarballBytes 是 client 端 tar.gz 包体上限,对齐 OAPI 设计 20MB 约束。
|
||
// 用 var 而非 const,便于单测调小覆盖拦截路径。
|
||
var maxHTMLPublishTarballBytes int64 = 20 * 1024 * 1024
|
||
|
||
// maxHTMLPublishRawBytes caps the total UNCOMPRESSED candidate size before
|
||
// tar+gzip writes them into the in-memory buffer. Defends against
|
||
// highly-compressible "decompression bomb" inputs (e.g. 50GB of zeros)
|
||
// that would balloon process memory before the gzip-after check fires.
|
||
// 200MB is much higher than any plausible legitimate HTML/static-site
|
||
// payload but low enough to stay well under typical container memory.
|
||
// Mutable for tests.
|
||
var maxHTMLPublishRawBytes int64 = 200 * 1024 * 1024
|
||
|
||
// maxHTMLPublishSingleHTMLFileBytes 单个 .html 文件上限,对齐妙搭服务端 10MB 约束。
|
||
// 用 var 而非 const,便于单测调小覆盖拦截路径。
|
||
var maxHTMLPublishSingleHTMLFileBytes int64 = 10 * 1024 * 1024
|
||
|
||
// oversizeHTMLFiles 返回 candidates 中扩展名为 .html(大小写不敏感)且单个 Size 超过
|
||
// maxHTMLPublishSingleHTMLFileBytes 的 RelPath 列表。只针对 .html 文件,不波及图片/字体/JS。
|
||
func oversizeHTMLFiles(candidates []htmlPublishCandidate) []string {
|
||
var hits []string
|
||
for _, c := range candidates {
|
||
if strings.EqualFold(filepath.Ext(c.RelPath), ".html") && c.Size > maxHTMLPublishSingleHTMLFileBytes {
|
||
hits = append(hits, c.RelPath)
|
||
}
|
||
}
|
||
return hits
|
||
}
|
||
|
||
// oversizeHTMLFilesError 构造单文件超限的 Validate 风格拒绝。
|
||
func oversizeHTMLFilesError(hits []string) error {
|
||
return appsValidationParamError("--path",
|
||
"--path contains %d HTML file(s) exceeding the %d bytes (10MB) per-file limit: %s",
|
||
len(hits), maxHTMLPublishSingleHTMLFileBytes, truncatedJoin(hits, maxSensitiveListInError)).
|
||
WithHint("split or trim oversized HTML file(s); the 10MB cap applies to each single .html file")
|
||
}
|
||
|
||
// ensureIndexHTML 要求 walker 抓到的 candidates 里必须含 index.html。
|
||
// 目录形态:根目录下必须有 index.html。
|
||
// 单文件形态:文件名必须就是 index.html。
|
||
// 妙搭服务端用 index.html 作为应用入口。
|
||
func ensureIndexHTML(candidates []htmlPublishCandidate) error {
|
||
for _, c := range candidates {
|
||
if c.RelPath == "index.html" {
|
||
return nil
|
||
}
|
||
}
|
||
return appsFailedPreconditionParamError("--path", "--path is missing index.html").
|
||
WithHint("index.html is the app entrypoint; for a directory put index.html at the root, or pass a single file named index.html")
|
||
}
|
||
|
||
// prepareHTMLPublishTarball validates candidates under path and builds a
|
||
// tar.gz payload ready for upload. Shared by runHTMLPublish and
|
||
// runHTMLPublishTOS.
|
||
func prepareHTMLPublishTarball(fio fileio.FileIO, path string) (*htmlPublishTarball, error) {
|
||
candidates, err := walkHTMLPublishCandidates(fio, path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if err := ensureIndexHTML(candidates); err != nil {
|
||
return nil, err
|
||
}
|
||
if hits := oversizeHTMLFiles(candidates); len(hits) > 0 {
|
||
return nil, oversizeHTMLFilesError(hits)
|
||
}
|
||
var rawTotal int64
|
||
for _, c := range candidates {
|
||
rawTotal += c.Size
|
||
}
|
||
if rawTotal > maxHTMLPublishRawBytes {
|
||
return nil, appsValidationParamError("--path",
|
||
"--path total raw bytes %d exceeds %d bytes limit (uncompressed pre-pack cap)", rawTotal, maxHTMLPublishRawBytes).
|
||
WithHint("reduce --path contents or choose a smaller subdirectory before packaging")
|
||
}
|
||
tarball, err := buildHTMLPublishTarball(fio, candidates)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if tarball.Size > maxHTMLPublishTarballBytes {
|
||
return nil, appsValidationParamError("--path",
|
||
"packed tar.gz size %d bytes exceeds %d bytes limit", tarball.Size, maxHTMLPublishTarballBytes).
|
||
WithHint("reduce --path contents, remove unrelated large files, then retry")
|
||
}
|
||
return tarball, nil
|
||
}
|
||
|
||
// runHTMLPublishTOS handles the publish path: validate → tar.gz →
|
||
// call pre_release to get TOS upload URL → upload tar.gz to TOS → return
|
||
// tos_path for +release-create --tos-path.
|
||
func runHTMLPublishTOS(ctx context.Context, rctx *common.RuntimeContext, spec appsHTMLPublishSpec) (map[string]interface{}, error) {
|
||
tarball, err := prepareHTMLPublishTarball(rctx.FileIO(), spec.Path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// Step 1: call pre_release to get TOS upload URL and tos_path.
|
||
preReleasePath := fmt.Sprintf("%s/apps/%s/pre_release", apiBasePath, validate.EncodePathSegment(spec.AppID))
|
||
preData, err := rctx.CallAPITyped("GET", preReleasePath, nil, nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
kvs, _ := preData["kvs"].([]interface{})
|
||
if len(kvs) == 0 {
|
||
return nil, appsSubprocessEnvelopeError("pre_release returned no kvs")
|
||
}
|
||
kvm := make(map[string]string, len(kvs))
|
||
for _, item := range kvs {
|
||
kv, _ := item.(map[string]interface{})
|
||
if kv == nil {
|
||
continue
|
||
}
|
||
k, _ := kv["key"].(string)
|
||
v, _ := kv["value"].(string)
|
||
if k != "" {
|
||
kvm[k] = v
|
||
}
|
||
}
|
||
uploadURL := kvm["upload_url"]
|
||
tosPath := kvm["tos_path"]
|
||
if uploadURL == "" || tosPath == "" {
|
||
return nil, appsSubprocessEnvelopeError("pre_release kvs missing upload_url or tos_path")
|
||
}
|
||
|
||
// Step 2: upload tar.gz to TOS via presigned URL (bypasses Lark gateway).
|
||
//nolint:forbidigo // presigned TOS upload bypasses the Lark gateway — raw http is required; not a Lark API call, so RuntimeContext.DoAPI does not apply.
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, uploadURL, bytes.NewReader(tarball.Body))
|
||
if err != nil {
|
||
return nil, errs.NewNetworkError(errs.SubtypeNetworkTransport, "build TOS upload request").WithCause(err)
|
||
}
|
||
req.ContentLength = tarball.Size
|
||
req.Header.Set("Content-Type", "application/gzip")
|
||
resp, err := newFileTransferClient().Do(req) //nolint:forbidigo // presigned TOS upload, see above.
|
||
if err != nil {
|
||
return nil, errs.NewNetworkError(errs.SubtypeNetworkTransport, "TOS upload failed").WithCause(err).WithRetryable()
|
||
}
|
||
defer resp.Body.Close()
|
||
if resp.StatusCode >= 400 {
|
||
if resp.StatusCode >= 500 {
|
||
return nil, errs.NewNetworkError(errs.SubtypeNetworkServer, "TOS upload failed: HTTP %d", resp.StatusCode).WithRetryable()
|
||
}
|
||
return nil, errs.NewNetworkError(errs.SubtypeNetworkTransport, "TOS upload failed: HTTP %d", resp.StatusCode)
|
||
}
|
||
|
||
// Step 3: call release-create with tos_path to trigger deployment.
|
||
releasePath := fmt.Sprintf(releaseCreatePath, validate.EncodePathSegment(spec.AppID))
|
||
releaseData, err := rctx.CallAPITyped("POST", releasePath, nil, map[string]interface{}{
|
||
"tos_path": tosPath,
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return map[string]interface{}{
|
||
"release_id": common.GetString(releaseData, "release_id"),
|
||
}, nil
|
||
}
|