Add azd ai agent pack and publish for Teams (activity) agents - #9332
Add azd ai agent pack and publish for Teams (activity) agents#9332v1212 wants to merge 18 commits into
Conversation
For activity-protocol agents, `azd deploy` postdeploy now calls the Foundry
Microsoft 365 packaging API (`/agents/{name}/microsoft365/zip`) to download a
ready-to-sideload Teams app package (`appPackage.zip`) next to the agent source,
right after it creates the Azure Bot + Teams channel. The service builds the
manifest, icons, and bot entry (botId = agent instance identity), so users no
longer assemble a Teams manifest by hand.
`TEAMS_APP_SETUP.md` now leads with sideloading the generated package (Teams UI
or `atk --scope Personal`, no Teams admin needed) and only falls back to the
manual packaging steps when package generation fails.
Additive and best-effort: packaging failures are logged and never fail the
deploy, and non-activity agents are completely unaffected. Publishing to the org
catalog (Shared/Tenant, admin-gated) is intentionally out of scope.
- Add agent_api.DownloadTeamsAppPackage + TeamsAppPackageRequest (+ tests)
- Add botservice.BotArmID helper (+ test)
- Wire writeTeamsAppPackage into ensureActivityBot; thread the package into the
guide/next-steps (+ updated tests)
- Rewrite the embedded guide with a generated-package vs manual fallback branch
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 7b480c5f-6c14-4d28-a19a-634f28621671
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b480c5f-6c14-4d28-a19a-634f28621671
|
Azure Pipelines: Successfully started running 1 pipeline(s). 21 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Adds best-effort generation of a sideloadable Teams app package after deploying activity-protocol agents.
Changes:
- Adds Microsoft 365 package download and Azure Bot ARM ID helpers.
- Writes
appPackage.zipand updates Teams setup guidance. - Adds unit tests and release notes.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
internal/pkg/botservice/botservice.go |
Builds Azure Bot ARM IDs. |
internal/pkg/botservice/botservice_test.go |
Tests ARM ID generation. |
internal/pkg/agents/agent_api/microsoft365.go |
Calls the Microsoft 365 packaging API. |
internal/pkg/agents/agent_api/microsoft365_test.go |
Tests packaging API requests and errors. |
internal/cmd/listen_activity.go |
Generates packages and presents next steps. |
internal/cmd/listen_activity_test.go |
Tests generated and fallback guidance. |
internal/cmd/assets/teams_app_setup_guide.md |
Documents sideloading and manual fallback. |
CHANGELOG.md |
Adds an unreleased feature entry. |
…tion The postdeploy hook now downloads a ready-to-sideload Teams app package, so the doc comment claiming packaging stays out of azd was stale. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b480c5f-6c14-4d28-a19a-634f28621671
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (5)
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/microsoft365.go:103
- [azd-code-reviewer] Route this finite request through
AgentClient.pipelineinstead of a freshhttp.Client.NewAgentClientconfigures bearer auth, Azure SDK retries/logging, correlation IDs, and the user agent inoperations.go:61-84; bypassing it means transient 429/5xx responses immediately force the manual fallback and this call lacks the correlation behavior used by the other agent APIs. Build a runtime request, set its JSON body and feature header, and callc.pipeline.Do.
httpClient := &http.Client{}
resp, err := httpClient.Do(req)
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:275
- [azd-code-reviewer] If the package write succeeds but the guide write fails, this prints a ready package and then the
guidePath == ""branch tells the user to package the app manually. Branch onpackagePaththere so this partial-success case gives sideloading instructions rather than contradictory guidance.
if packagePath != "" {
fmt.Printf(" Teams app: %s (ready to sideload)\n", packagePath)
cli/azd/extensions/azure.ai.agents/CHANGELOG.md:7
- [azd-code-reviewer] Remove this entry from the feature PR. The extension's release workflow reserves
CHANGELOG.mdupdates for the dedicated version-bump PR that also changesversion.txtandextension.yaml(cli/azd/extensions/azure.ai.agents/AGENTS.md:123-139); adding it here creates release-time merge conflicts and separates the entry from its version bump.
## 1.0.0-beta.8 (Unreleased)
### Features Added
- [[#9332]](https://github.com/Azure/azure-dev/pull/9332) For activity-protocol agents, `azd deploy` now generates a ready-to-sideload Teams app package (`appPackage.zip`) next to the agent source by calling the Foundry Microsoft 365 packaging API, so you no longer have to assemble a Teams manifest and icons by hand. `TEAMS_APP_SETUP.md` now leads with sideloading that package (Teams UI or the `atk` CLI, `--scope Personal`, no Teams admin needed) and only falls back to manual packaging steps if package generation fails. Non-activity agents are unaffected.
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:169
- [azd-code-reviewer]
writeTeamsAppPackageis the central integration path but has no test. The lower-level API and ARM-ID tests do not verify the destination path, request metadata, successful file write, or the promised empty-string fallback on API/write failures. Add temp-directory andhttptestcoverage, consistent with the existingwriteTeamsSetupGuidetest in this package.
func writeTeamsAppPackage(
ctx context.Context,
agentClient *agent_api.AgentClient,
proj *azdext.ProjectConfig,
svc *azdext.ServiceConfig,
agentName, subscriptionID, resourceGroup, botName string,
) string {
cli/azd/extensions/azure.ai.agents/internal/cmd/assets/teams_app_setup_guide.md:29
- [azd-code-reviewer] This copy-paste command resolves
appPackage.ziprelative to the user's current shell directory, butazd deploynormally leaves the shell at the project root while the package may be under the service'sproject:path. For services such assrc/agent, the command fails unless the user changes directory. State that these commands must run from the directory containing this guide/package, or render a quoted path that works from the project root.
atk install --file-path {{.PackageFile}} --scope Personal
- Route the Microsoft 365 pack call through the shared client pipeline (retry/ correlation/bearer policies) instead of a bare http.Client, so transient 429/5xx are retried rather than dropping straight to the manual fallback. - Write the package atomically (temp file + rename) and remove any stale appPackage.zip on fallback, so a failed run can't leave a partial or mismatched zip that the guide would point users at. - When the guide write fails but the package exists, direct users to sideload the generated package instead of packaging manually. - Drop the CHANGELOG entry; changelog updates are deferred to the release PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b480c5f-6c14-4d28-a19a-634f28621671
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:218
- This reimplements the repository's atomic writer with a shared fixed
.tmpname and directos.Rename, missing the unique temp file, fsync, cleanup, and Windows transient-lock retries provided byazdext.WriteFileAtomic(cli/azd/pkg/azdext/atomicfile.go:20-94). Reuse that helper so simultaneous writers cannot collide and a transient Windows file lock does not unnecessarily force the manual fallback. [azd-code-reviewer]
// Write atomically (temp file + rename) so an interrupted or failed write can
// never leave a partial/corrupt zip that the guide would point users at.
tmpPath := packagePath + ".tmp"
if err := os.WriteFile(tmpPath, zipBytes, 0o600); err != nil {
The generated appPackage.zip lives in the user's source dir under a generic name, so a user may keep their own manually assembled package there. Track azd ownership with a sidecar marker (.appPackage.zip.azd-generated): azd now only overwrites or removes the package when the marker is present, preserving an unowned user file on both the success and fallback paths. Add regression tests covering preserve-unowned, write-when-absent, overwrite-owned, and remove-owned. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b480c5f-6c14-4d28-a19a-634f28621671
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (6)
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:214
- [azd-code-reviewer] Local commit failures skip the stale-package cleanup used for API failures. If an owned package from a previous deploy exists and the temp write or rename fails, that stale ZIP and marker remain even though the guide switches to manual fallback; its bot binding may no longer match this deployment. Remove the owned artifact before returning here.
if err != nil {
log.Printf("postdeploy: failed to write Teams app package %q: %v", packagePath, err)
return ""
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:241
- [azd-code-reviewer] The predictable
.tmppath bypasses the project path's symlink validation. A checked-out repository can containappPackage.zip.tmpas a symlink outside the project, andos.WriteFilefollows it and truncates the target during deploy. Create a unique temp file in the package directory, close it, and rename it instead.
tmpPath := packagePath + ".tmp"
if err := os.WriteFile(tmpPath, zipBytes, 0o600); err != nil {
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:251
- [azd-code-reviewer]
os.WriteFilefollows a pre-existing marker symlink. BecausemarkerPathis not validated when the package is absent, a repository containing.appPackage.zip.azd-generatedas a symlink can make a successful deploy truncate a file outside the project;teamsAppPackageIsOwnedalso trusts that symlink viaos.Stat. Write the marker through a same-directory temp file plus rename, and useos.Lstatto reject non-regular ownership markers.
if err := os.WriteFile(markerPath, []byte("generated by azd\n"), 0o600); err != nil {
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:156
- [azd-code-reviewer]
appPackage.zipis scoped only by the service directory, but this extension supports multiple agent services sharing the sameRelativePath(for example,helpers_test.go:880-881), and service deploy steps can run concurrently (cli/azd/AGENTS.md:214-220). Those agents race on the same package and marker, so the final ZIP can belong to a different bot than an earlier service's output claims. Namespace the package and marker by service/agent, or use a per-service output directory, and cover a shared-root multi-agent deploy.
const teamsAppPackageFile = "appPackage.zip"
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:271
- [azd-code-reviewer] If deleting the stale package fails, this still removes its ownership marker. The stale ZIP then becomes permanently "unowned", so later deploys refuse to replace it and keep falling back to manual packaging. Preserve the marker when package deletion fails so a future run can retry cleanup.
if err := os.Remove(packagePath); err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("postdeploy: could not remove stale Teams app package %q: %v", packagePath, err)
}
if err := os.Remove(markerPath); err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("postdeploy: could not remove Teams app package marker %q: %v", markerPath, err)
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:202
- [azd-code-reviewer] This treats cancellation of the parent deploy context as an ordinary packaging failure. If the user cancels during this network call, the hook continues writing fallback files and returns success instead of stopping. Keep the 60-second packaging timeout best-effort, but when the parent
ctx.Err()is non-nil, propagate that cancellation throughensureActivityBot.
if err != nil {
Add two explicit, loud commands for Activity (Teams) agents that complement the deploy-time best-effort packaging: - `azd ai agent pack` downloads a ready-to-sideload Teams app package (appPackage.zip) from the Foundry Microsoft 365 zip endpoint for local customization and `atk install --scope personal` sideloading. - `azd ai agent publish` builds and publishes the package to the Microsoft 365 store. Defaults to `shared` scope (shareable install link, no tenant-admin approval) and supports `org` scope (organization catalog, admin-gated). `personal` is rejected with guidance to use pack + sideload, since per-user install is a Teams client action, not a store publish. Both require a prior `azd deploy` (the Azure Bot they bind to is created then) and are activity-agent only. Unlike the postdeploy hook, platform failures (tenant policy, permissions, service errors) surface as command failures instead of silent fallbacks. - Add agent_api.PublishTeamsApp + TeamsAppPublishResult - Add shared resolveTeamsPackContext / scope + request helpers (+ tests) - Register pack/publish in root; add exterrors codes/ops Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b480c5f-6c14-4d28-a19a-634f28621671
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Suppressed comments (5)
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:262
- Treating the sidecar as best-effort breaks the ownership invariant. If the ZIP succeeds but this write fails, the command reports success; later runs treat azd's own ZIP as user-owned, refuse to update it, and cannot remove it as stale. Make marker creation part of the commit and roll back the ZIP on failure. [azd-code-reviewer]
// Best-effort marker: if it can't be written the zip is still valid; azd just
// won't recognize the file as its own on a later run.
if err := os.WriteFile(markerPath, []byte("generated by azd\n"), 0o600); err != nil {
log.Printf("postdeploy: could not write Teams app package ownership marker %q: %v", markerPath, err)
}
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:294
- If removing the ZIP fails, this still deletes its ownership marker. That permanently converts the stale azd-generated package into an “unowned” file, so future runs preserve it and refuse to overwrite it. Keep the marker when package removal fails so cleanup can be retried. [azd-code-reviewer]
if err := os.Remove(packagePath); err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("postdeploy: could not remove stale Teams app package %q: %v", packagePath, err)
}
if err := os.Remove(markerPath); err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("postdeploy: could not remove Teams app package marker %q: %v", markerPath, err)
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/microsoft365.go:161
- A syntactically valid
200 {}is currently treated as a successful publish, after which the command prints empty IDs and constructs a useless deep link. Validate both required IDs after unmarshalling and fail transparently when either is absent. [azd-code-reviewer]
var result TeamsAppPublishResult
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse Teams app publish response: %w", err)
}
return &result, nil
cli/azd/extensions/azure.ai.agents/internal/cmd/teams_pack.go:212
- Both callers apply the agent-name default before this trim. Therefore
--display-name " "reaches this shared builder as non-empty, is trimmed to empty here, and produces blank display metadata and descriptions. Trim before fallback, or pass the default agent name into this builder and apply it here. [azd-code-reviewer]
displayName := strings.TrimSpace(opts.displayName)
cli/azd/extensions/azure.ai.agents/internal/cmd/publish.go:152
- This adds a machine-consumed JSON contract, but no automated test executes publish output and asserts clean parseable JSON plus
titleId,teamsAppId,scope, anddeepLink. Repository guidance requires tests for newly added JSON fields; add a command/action test that also ensures human progress text is absent. [azd-code-reviewer]
payload := map[string]string{
"titleId": result.TitleID,
"teamsAppId": result.TeamsAppID,
"scope": scope.flag,
"deepLink": deepLink,
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Suppressed comments (4)
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:307
- If removing the stale package fails, this continues and removes its ownership marker. The stale ZIP then remains but is treated as user-owned, so later deploys cannot replace it; preserve the marker when package deletion fails. [azd-code-reviewer]
if err := os.Remove(packagePath); err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("postdeploy: could not remove stale Teams app package %q: %v", packagePath, err)
}
if err := os.Remove(markerPath); err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("postdeploy: could not remove Teams app package marker %q: %v", markerPath, err)
cli/azd/extensions/azure.ai.agents/internal/cmd/teams_pack_test.go:81
- This line exceeds the repository's 125-column Go limit when tabs are counted at width 4, so
golangci-lint'slllcheck will fail. Split the URL across concatenated strings as required bycli/azd/AGENTS.md:97-108.
want := "https://teams.microsoft.com/v2/#/l/app/?source=agent-details-page&titleId=T_abc+123&launchAgent=join_launcher_web"
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/microsoft365.go:161
- A successful status with
{}or a partial payload is accepted, so the command prints success and builds an invalid install link. Validate both required IDs before returning the result to preserve the PR's failure-transparency guarantee. [azd-code-reviewer]
var result TeamsAppPublishResult
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse Teams app publish response: %w", err)
}
return &result, nil
cli/azd/extensions/azure.ai.agents/internal/cmd/publish.go:147
- The new
--output jsoncontract has no command-level test; the API tests do not verify pure stdout or these four fields. Add an action/command test that captures stdout, parses it as JSON, and assertstitleId,teamsAppId,scope, anddeepLinkwith no progress text, as required for new JSON fields by the repository testing guidance. [azd-code-reviewer]
if a.flags.output == "json" {
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.
Suppressed comments (5)
cli/azd/extensions/azure.ai.agents/internal/cmd/publish.go:152
- The new machine-readable contract is not covered by an automated test. This package already captures stdout and asserts JSON shapes (for example,
show_test.go:90-98), and the repository requires tests for new JSON fields. Extract/test this output path so a test unmarshals and verifiestitleId,teamsAppId,scope, anddeepLink, and confirms progress text does not contaminate JSON stdout. [azd-code-reviewer]
payload := map[string]string{
"titleId": result.TitleID,
"teamsAppId": result.TeamsAppID,
"scope": scope.flag,
"deepLink": deepLink,
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/microsoft365.go:164
- A successful status with
{}or either identifier omitted is accepted here.PublishActionthen reports success and builds an unusable deep link from an empty title ID, even though both IDs are required by this response contract. ValidatetitleIdandteamsAppIdbefore returning. [azd-code-reviewer]
var result TeamsAppPublishResult
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse Teams app publish response: %w", err)
}
return &result, nil
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:321
- If deleting the package fails (for example, because Windows still has it open), this still deletes the ownership marker. That leaves the stale ZIP permanently classified as user-owned, so later deploys cannot retry cleanup or overwrite it. Keep the marker when package removal fails. [azd-code-reviewer]
if err := os.Remove(packagePath); err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("postdeploy: could not remove stale Teams app package %q: %v", packagePath, err)
}
if err := os.Remove(markerPath); err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("postdeploy: could not remove Teams app package marker %q: %v", markerPath, err)
cli/azd/extensions/azure.ai.agents/internal/cmd/teams_pack_test.go:81
- This line is 127 columns at the configured tab width, exceeding the 125-character Go limit enforced by
lll(cli/azd/AGENTS.md:97,107-108), so preflight will fail. Split the expected URL across lines. [azd-code-reviewer]
want := "https://teams.microsoft.com/v2/#/l/app/?source=agent-details-page&titleId=T_abc+123&launchAgent=join_launcher_web"
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:204
- This duplicates the package-request defaults now centralized in
buildTeamsAppPackageRequest, including descriptions, developer URLs, version, and mention behavior. The deploy-time and explicit command paths can silently produce different manifests when either copy changes. Build the personal-scope request through the shared helper as well. [azd-code-reviewer]
request := agent_api.TeamsAppPackageRequest{
BotServiceArmID: botservice.BotArmID(subscriptionID, resourceGroup, botName),
PublishScope: "Personal",
AgentDisplayName: agentName,
AppVersion: "1.0.0",
Treat ownership marker write failures as package commit failures so retries cannot leave an unowned generated package behind. Roll back newly created packages on marker failure while preserving packages that already existed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b480c5f-6c14-4d28-a19a-634f28621671
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (2)
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:236
- The sidecar only proves that azd generated this path once; it does not prove the current ZIP is still azd-generated. The pack workflow is explicitly intended to let users customize
appPackage.zip, but a customized file leaves this hidden marker behind, so the next deploy overwrites it and a packaging failure deletes it viaremoveOwnedTeamsAppPackage. Track the generated ZIP's digest in the marker and treat a digest mismatch as user-owned, or keep managed output separate from the user's customizable copy. [azd-code-reviewer]
// teamsAppPackageIsOwned reports whether appPackage.zip was generated by azd, as
// recorded by the sidecar marker. azd only removes or overwrites packages it owns.
func teamsAppPackageIsOwned(markerPath string) bool {
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:171
- These new files are written into the service source directory, but
DefaultAgentIgnoreContentdoes not excludeappPackage.zip, its marker, orTEAMS_APP_SETUP.md.createCodeDeployZipwalks that directory and includes every non-ignored file (internal/project/service_target_agent.go:1788-1836), so every later code deploy uploads the Teams distribution package and setup artifacts as agent source. Exclude these generated artifacts from deployment packaging, including for projects with an existing custom.agentignore. [azd-code-reviewer]
// teamsAppPackageFile is the name of the generated, ready-to-sideload Teams app package.
const teamsAppPackageFile = "appPackage.zip"
jongio
left a comment
There was a problem hiding this comment.
Two non-blocking items:
microsoft365.go:PublishTeamsAppcan return a result with an emptyTitleID, and the install link is now built from it.listen_activity.go: the generatedappPackage.zip, its marker, andTEAMS_APP_SETUP.mdaren't in the default.agentignore, so later code deploys bundle them as agent source.
Reject publish responses that are empty or missing titleId so the command does not print an unusable Teams link. Always exclude generated Teams package artifacts from code deploy packaging, even when existing projects have a custom .agentignore. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b480c5f-6c14-4d28-a19a-634f28621671
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 15 out of 15 changed files in this pull request and generated no new comments.
Suppressed comments (3)
cli/azd/extensions/azure.ai.agents/internal/cmd/publish.go:153
- The new
--output jsoncontract is not covered by an automated test, although it promises pure JSON and four specific fields. Add a test that captures stdout, parses it, and assertstitleId,teamsAppId,scope, anddeepLinkwith no progress text; extracting the rendering step or injecting the client would make this testable without a live service. [azd-code-reviewer]
if a.flags.output == "json" {
payload := map[string]string{
"titleId": result.TitleID,
"teamsAppId": result.TeamsAppID,
"scope": scope.flag,
"deepLink": deepLink,
}
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:204
- This repeats the full package metadata assembly already centralized in
buildTeamsAppPackageRequest. The guarantee that deploy-time packaging andagent packproduce the same package now depends on keeping these copied fields synchronized. Route this path through the shared builder (with the personal scope) so future metadata changes cannot silently diverge. [azd-code-reviewer]
request := agent_api.TeamsAppPackageRequest{
BotServiceArmID: botservice.BotArmID(subscriptionID, resourceGroup, botName),
PublishScope: "Personal",
AgentDisplayName: agentName,
AppVersion: "1.0.0",
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/microsoft365.go:170
- A
200response that containstitleIdbut omitsteamsAppIdis currently treated as success, so both human and JSON output report an empty Teams App ID despite the command's documented result contract. Validate both required IDs before returning. [azd-code-reviewer]
if result.TitleID == "" {
return nil, fmt.Errorf("Teams app publish response was missing titleId")
}
Honor --agent-name when initializing from a unified azure.yaml sample by writing the chosen name back to the adopted agent service before existing-agent conflict checks run. Also pin the interactive unified-template name so the adopted project uses the selected agent identity. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b480c5f-6c14-4d28-a19a-634f28621671
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
cli/azd/extensions/azure.ai.agents/internal/cmd/teams_pack.go:180
- The selected environment's deployment state can belong to a different tenant, but
newAgentCredential()requests a token without that environment'sAZURE_TENANT_ID. The deploy path already scopes this credential to the environment tenant inlisten.go:377-389; read the same value here and create a tenant-scoped credential, otherwise pack and publish can fail with 401/403 for valid multi-tenant projects. [azd-code-reviewer]
credential, err := newAgentCredential()
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/microsoft365.go:170
- The publish contract requires both IDs, but only
titleIdis validated. A 200 response withoutteamsAppIdtherefore reports success and emits an empty Teams App ID, including in JSON output. Reject the incomplete response and add the corresponding missing-field test. [azd-code-reviewer]
if result.TitleID == "" {
return nil, fmt.Errorf("Teams app publish response was missing titleId")
}
cli/azd/extensions/azure.ai.agents/internal/cmd/publish.go:152
- This introduces a machine-readable output contract without an automated test for its exact fields or JSON purity. Add a focused formatter/command test that parses stdout and asserts
titleId,teamsAppId,scope, anddeepLink, with no progress text;cli/azd/AGENTS.md:378explicitly requires tests for new JSON fields. [azd-code-reviewer]
if a.flags.output == "json" {
payload := map[string]string{
"titleId": result.TitleID,
"teamsAppId": result.TeamsAppID,
"scope": scope.flag,
"deepLink": deepLink,
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7b480c5f-6c14-4d28-a19a-634f28621671
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.
Suppressed comments (6)
cli/azd/extensions/azure.ai.agents/internal/project/agentignore.go:80
- With a custom
.agentignore, this exact-path lookup only protects the source root.pack --output-dir ./distcan generatedist/appPackage.zipinside the service source, and the next code deploy will include it despite this artifact being declared “always excluded.” Match these reserved names by basename and add a nested-path regression case. [azd-code-reviewer]
if _, ok := alwaysExcludedFromCodeDeploy[relPath]; ok {
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/microsoft365.go:170
- The response is accepted when
teamsAppIdis missing, sopublishreports success and emits an empty ID even though that field is part of the command’s promised result. Validate both required identifiers before returning, and cover the missing-ID response. [azd-code-reviewer]
if result.TitleID == "" {
return nil, fmt.Errorf("Teams app publish response was missing titleId")
}
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go:659
- This conflict is detected after
scaffoldProjecthas already written the adoptedazure.yaml, so rerunning init in the same target fails the existing-project check before the override is applied. The advice also cannot work for multi-agent samples becauseapplyAdoptedAgentNameOverrideexplicitly rejects them. Keep the actionable edit-in-place guidance, or explain the required cleanup/new target and multi-agent path. [azd-code-reviewer]
return "To create a separate agent, re-run init with --agent-name <unique-name>.\n"
cli/azd/extensions/azure.ai.agents/internal/cmd/listen_activity.go:334
- If deleting the stale package fails (for example, because of a transient Windows file lock), execution still removes its ownership marker. That leaves the stale ZIP classified as user-owned, so subsequent deploys refuse to replace or remove it. Only remove the marker after the package was removed or was already absent. [azd-code-reviewer]
if err := os.Remove(packagePath); err != nil && !errors.Is(err, os.ErrNotExist) {
log.Printf("postdeploy: could not remove stale Teams app package %q: %v", packagePath, err)
cli/azd/extensions/azure.ai.agents/internal/cmd/teams_pack.go:106
nameis matched against theazure.yamlservice name here, but both commands describe the positional value as an agent name. Because the deployed Foundry name can differ (and is separately read fromAGENT_<SERVICE>_NAMEbelow), passing that displayed agent name produces “no service found.” Document[name]as the service name in both commands’ help/examples, or support both forms without ambiguity. [azd-code-reviewer]
svc, proj, err := resolveAgentService(ctx, azdClient, name, noPrompt)
cli/azd/extensions/azure.ai.agents/internal/cmd/publish.go:152
- This new machine-readable contract has no automated command-output test. Add coverage that captures stdout, parses it as JSON, asserts all four fields and their formats, and verifies that progress text is absent; the repository guidance explicitly requires tests for new JSON fields. [azd-code-reviewer]
payload := map[string]string{
"titleId": result.TitleID,
"teamsAppId": result.TeamsAppID,
"scope": scope.flag,
"deepLink": deepLink,
jongio
left a comment
There was a problem hiding this comment.
Rechecked after the latest push. The two items from my last pass are genuinely fixed, and build plus tests are green.
Two notes on the new --agent-name adopt override and one carried over on the always-excluded list. All inline, none blocking.
|
|
||
| func adoptedAgentNameConflictSuggestion() string { | ||
| return "To create a separate agent, update the agent service's `name` in the adopted azure.yaml before deploying.\n" | ||
| return "To create a separate agent, re-run init with --agent-name <unique-name>.\n" |
There was a problem hiding this comment.
This suggestion now points people at --agent-name, but updateAdoptedAgentNames emits it for every conflicting agent service, including azure.yaml files that declare more than one. In that case applyAdoptedAgentNameOverride hard-fails with CodeConflictingArguments ("cannot be applied to an adopted azure.yaml with multiple agent services"), so following the advice dead-ends. A single flag also can't express which of several services the name should land on.
Since this is the --no-prompt suggestion, the CI path is where it hurts most. Making the wording conditional on the agent-service count, or keeping the azure.yaml edit guidance as a second line, would keep it actionable in both shapes.
| serviceName = name | ||
| configPath = path | ||
| } | ||
| if serviceName == "" { |
There was a problem hiding this comment.
When no agent service carries a name, an explicit --agent-name gets dropped silently here. By this point runInitFromAzureYaml has already applied it to the target directory via projectName = validatedName, so the folder ends up named after the requested agent while azure.yaml keeps the sample's own name.
That half-applied outcome is hard to notice. A warning, or an error, would make it obvious that the flag didn't reach azure.yaml.
| // relPath is the path relative to srcDir using forward slashes. | ||
| // isDir indicates whether the path is a directory. | ||
| func (m *agentIgnoreMatcher) ShouldExclude(relPath string, isDir bool) bool { | ||
| if !isDir { |
There was a problem hiding this comment.
This runs before the matcher, so the negation escape hatch that DefaultAgentIgnoreContent() documents a few lines down ("To include a file that is excluded by default, use negation: !filename") silently doesn't work for these three names.
It's also an exact root-relative match, while the gitignore pattern appPackage.zip in the default template matches at any depth. So with a custom .agentignore, a nested package from azd ai agent pack --output-dir ./build still gets bundled as source.
Prepending these three patterns to the user's ignore content instead of short-circuiting here would cover both: patterns without a slash already match at any depth, and a later !appPackage.zip could still win if someone really wants it.
Fixes #9400
Related to #6752
Summary
Adds two explicit commands for Activity (Teams) agents. Both require a prior
azd deploy(the Azure Bot they bind to is created then) and surface platform failures (tenant policy, permissions, service errors) as command failures rather than silent fallbacks.Design: two independent commands
azd ai agent packDownloads a ready-to-sideload Teams app package (
appPackage.zip) from the Foundry Microsoft 365 zip endpoint. Intended for the developer inner loop: customize the package and/or sideload it for local testing withatk install --scope personal(no Teams admin required).[name] --scope --display-name --app-version --output-dirpersonalpersonal,shared,tenant(orgis accepted as a compatibility alias fortenant).appPackage.zipnext to the agent source (or--output-dir).azd ai agent publishBuilds the same package server-side and publishes it to the Microsoft 365 store, returning the title id, Teams app id, and install deep link.
[name] --scope --display-name --app-version --outputsharedshared— shareable install link; recipients can add the app without tenant-admin approval.tenant— organization-wide catalog; requires IT-admin approval before tenant users can use it in Teams.personalis not supported — per-user install is a Teams client action, not a store publish. For that path useazd ai agent pack+atk install --scope personal.--output jsonreturns pure JSON withtitleId,teamsAppId,scope, anddeepLink.Failure transparency
Unlike deploy-time best-effort packaging, these commands report backend errors faithfully. If the platform fails (for example, the Microsoft 365
/publishendpoint errors),azd ai agent publishfails with the underlying service error instead of silently skipping.Testing
go test ./internal/cmd ./internal/pkg/agents/agent_api ./internal/exterrorsgo fix -diff ./...reports no remaining suggestions.azd x pack --rebuild,azd x publish --source local, andazd extension install azure.ai.agents --source local --force.azd ai agent pack echopub073001 --scope personal --output-dir <tmp>:appPackage.zip.manifest.json,default-color-icon.png, anddefault-outline-icon.png.name.short=echopub073001,version=1.0.0,developer.name=Azure AI Foundry, andbotId=119aa5ee-f7db-41ce-a50c-9853b54133a5.azd ai agent publish echopub073001 --scope shared --no-prompt:titleId=T_7921a0e5-c58b-8dbc-d833-ed5ef671eec4.teamsAppId=040abe71-46ba-44ad-a7f1-8d7bdf426062.https://teams.microsoft.com/l/app/040abe71-46ba-44ad-a7f1-8d7bdf426062.azd ai agent publish echopub073001 --no-promptpublishes withscope: shared.azd ai agent publish echopub073001 --no-prompt -o jsonreturns parseable JSON.azd ai agent publish echopub073001 --scope personal --no-promptfails with an actionable message pointing topack+atk install --scope personal.hi, and receivedEcho : hifromechopub073001.Latest E2E validation
Validated with the PR-built local
azure.ai.agentsextension (1.0.0-beta.8, commitc55a3335f) against a fresh Activity echo project:azd ai agent initfrom the Activity quickstart echo sampleazd provision(new Foundry project + ACR)azd deploy(agentechopr933208032120, active version3)azd ai agent publish --scope shared --display-name echopr933208032120hireceivedEcho: hiPublish result used for validation:
{ "titleId": "T_8a6472dd-f4d3-4337-4be1-09da737a5cc2", "teamsAppId": "788aac2c-d433-4f94-84ac-97f68d3ed452" }