From 5bc9cb2c14f529a98d38c20c91700d20ea9db7a5 Mon Sep 17 00:00:00 2001 From: Alon Yeshurun Date: Sun, 2 Aug 2026 14:34:18 +0300 Subject: [PATCH 01/15] Draft Azure CLI authentication design AB#1694265 ## Summary Scaffold feature 1694265 and add the Fabric CLI-specific design and seven-slice implementation plan for Azure CLI authentication. ## Prompting Intent Draft the repo design spec from the Feature Registry artifacts, create an implementation plan and task breakdown, and prepare the design readiness gate required before syncing tasks to ADO. ## Linked Sources - Requirements spec: https://powerbi@dev.azure.com/powerbi/Trident/_git/PlatformDevFeatureRegistry?path=/Features/active/1694265/requirements-spec.md - Engineering design: https://powerbi@dev.azure.com/powerbi/Trident/_git/PlatformDevFeatureRegistry?path=/Features/active/1694265/engineering-design.md - Implementation handoff: https://powerbi@dev.azure.com/powerbi/Trident/_git/PlatformDevFeatureRegistry?path=/Features/active/1694265/implementation-handoff.md - Test plan: https://powerbi@dev.azure.com/powerbi/Trident/_git/PlatformDevFeatureRegistry?path=/Features/active/1694265/test-plan.md ## Rationale Translate the locked cross-cutting contract into concrete Fabric CLI modules and reviewable workstreams while leaving security, host-integration, and rollout decisions as explicit gates. Task work items are intentionally deferred until this design is merged, as required by the feature readiness policy. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9c368147-9689-45f9-9f29-fc882429424b --- .github/hooks/hooks.json | 15 ++ Features/1694265/design-spec.md | 229 ++++++++++++++++++++++++ Features/1694265/implementation-plan.md | 71 ++++++++ Features/1694265/registry.md | 20 +++ 4 files changed, 335 insertions(+) create mode 100644 .github/hooks/hooks.json create mode 100644 Features/1694265/design-spec.md create mode 100644 Features/1694265/implementation-plan.md create mode 100644 Features/1694265/registry.md diff --git a/.github/hooks/hooks.json b/.github/hooks/hooks.json new file mode 100644 index 00000000..f94d97b6 --- /dev/null +++ b/.github/hooks/hooks.json @@ -0,0 +1,15 @@ +{ + "version": 1, + "hooks": { + "sessionStart": [ + { + "type": "command", + "bash": "b=\"${HOME:-${USERPROFILE:-}}/.copilot/hooks\"; s=\"$b/.last-copilot-toolkit-plugin-update.$(date -u +%Y%m%d)\"; if mkdir -p \"$b\" 2>/dev/null && ( set -C; : > \"$s\" ) 2>/dev/null; then copilot plugin install https://dev.azure.com/msdata/A365/_git/copilot-toolkit 2>/dev/null || echo 'Plugin already installed or unavailable' >&2; fi", + "powershell": "try { $b = Join-Path $HOME '.copilot/hooks'; New-Item -ItemType Directory -Force -Path $b -ErrorAction Stop | Out-Null; $s = Join-Path $b ('.last-copilot-toolkit-plugin-update.' + [DateTime]::UtcNow.ToString('yyyyMMdd')); $fs = [System.IO.File]::Open($s, [System.IO.FileMode]::CreateNew); $fs.Close(); copilot plugin install https://dev.azure.com/msdata/A365/_git/copilot-toolkit 2>$null; if ($LASTEXITCODE -ne 0) { Write-Host 'Plugin already installed or unavailable' -ForegroundColor Yellow } } catch { }; exit 0", + "cwd": ".", + "timeoutSec": 120, + "comment": "Install/update Copilot Toolkit plugins at most once per day (daily stamp guard) to avoid Azure DevOps throttling" + } + ] + } +} diff --git a/Features/1694265/design-spec.md b/Features/1694265/design-spec.md new file mode 100644 index 00000000..c6a33752 --- /dev/null +++ b/Features/1694265/design-spec.md @@ -0,0 +1,229 @@ +# Design Spec — Feature 1694265 (fabric-cli) + +> Repo-specific design for adding Azure CLI as an explicit Fabric CLI authentication source. +> The parent [engineering design](https://powerbi@dev.azure.com/powerbi/Trident/_git/PlatformDevFeatureRegistry?path=/Features/active/1694265/engineering-design.md), [implementation handoff](https://powerbi@dev.azure.com/powerbi/Trident/_git/PlatformDevFeatureRegistry?path=/Features/active/1694265/implementation-handoff.md), and [test plan](https://powerbi@dev.azure.com/powerbi/Trident/_git/PlatformDevFeatureRegistry?path=/Features/active/1694265/test-plan.md) define the product contract. + +## Scope + +Fabric CLI owns the complete client-side implementation: + +- Add `azure-cli` as an explicit authentication source while preserving all existing direct Fabric CLI sources. +- Introduce a shared pre-dispatch authentication coordinator for command-line, batch, auth, and REPL execution. +- Acquire Azure CLI tokens noninteractively through `AzureCliCredential`. +- Persist a versioned source and identity binding, but never persist delegated Azure CLI tokens. +- Add passive and active authentication status, stable error/exit behavior, source-local logout, and source-aware SDK/deploy integration. +- Preserve the existing route labels and scopes: `fabric` and `powerbi` use the Fabric scope, `storage` uses the OneLake scope, and `azure` uses the ARM scope. +- Add feature gating, telemetry, documentation, and the cross-platform regression matrix required for rollout. + +### Non-goals + +- Running or wrapping `az login`, `az logout`, `az account set`, or any Azure CLI context mutation. +- Using `DefaultAzureCredential` or merging Azure CLI tokens into the MSAL cache. +- Persisting Azure CLI access or refresh tokens. +- Supporting arbitrary scopes, claims challenges, SQL, XMLA, Kusto, or non-Public Azure clouds in the first release. +- Normalizing every existing identity mode under the new `--source` syntax. + +## Current Architecture + +Authentication currently spans several independent paths: + +- `fabric_cli.main` special-cases auth commands and uses `_execute_command` only for other one-shot commands. +- `InteractiveCLI.handle_command` parses and invokes handlers independently. +- `FabAuth` combines persistent state, environment loading, provider selection, MSAL acquisition, and interactive renewal. +- `fab_api_client.do_request` acquires tokens during request execution, which can trigger interactive renewal. +- `fab_auth.status` requests three tokens, and `FabAuth.logout` resets unrelated configuration. +- `MsalTokenCredential` supports only the current Fabric CLI provider and remains headless. +- Config-file deploy creates the credential inside a catch-all that maps failures to `DeploymentFailed`. + +The implementation must separate policy, coordination, provider behavior, and persistence without regressing existing authentication modes. + +## Proposed Design + +### Shared parsed-command executor + +Create one executor used by one-shot, batch, auth, and REPL surfaces. It will: + +1. Classify the parsed command as local, passive auth, active auth, or authenticated. +2. Resolve interaction policy from command flags, output mode, host capability, CI/batch/pipe context, and `FAB_INTERACTION`. +3. Resolve the effective source using runtime environment overrides before the configured source. +4. Run authentication readiness before handler dispatch when required. +5. Invoke the parsed handler at most once and return its exit code without replay. + +Local commands such as help, version, passive status, and logout bypass token acquisition. Batch execution fails fast and reports executed, failed, and skipped counts. + +### Authentication coordinator + +Add a coordinator responsible for source resolution, interaction eligibility, chooser orchestration, candidate validation, atomic binding, and exactly-once continuation. Provider classes remain noninteractive. + +Source precedence: + +1. Runtime environment credentials for the current process. +2. Persisted configured source. +3. Shared chooser only when no source exists and interaction is allowed. +4. `AuthenticationRequired` for unattended or deferred execution. + +The coordinator exposes a small result model containing configured source, effective source, principal capability, readiness state, and optional checked-audience expiration. It does not expose token values. + +### Provider boundary + +Define a provider protocol used by `FabAuth`, HTTP requests, status checks, and the SDK bridge: + +- Acquire exactly one allowlisted audience. +- Return token and expiration metadata. +- Validate tenant and principal against the active binding. +- Clear only process-local cached data for a requested audience. +- Report stable Fabric CLI errors without raw SDK or process output. + +Existing MSAL user, service-principal, managed-identity, federation, certificate, and raw-token behavior remain behind the direct `fabric-cli` provider. Interactive MSAL renewal moves out of ordinary provider acquisition and is initiated only by the coordinator when policy allows it. + +### Azure CLI provider + +Add `azure-identity` as a dependency and implement the provider with `AzureCliCredential`: + +- Resolve only a trusted Azure CLI executable through Azure Identity. +- Use disconnected standard input, no shell, a safe working directory, and a bounded 10-second acquisition. +- Pass one resolved `.default` scope per call. +- Allow only `fabric`, `storage`, `azure`, and `powerbi` route labels. +- Keep `powerbi` mapped to the existing Fabric scope. +- Cache successful tokens in process by source, tenant, principal, and audience. +- Coalesce concurrent misses for the same key; refresh inside the configured buffer; never cache failures. +- Clear and reject only the affected token when a claims challenge is returned. + +The provider never invokes Azure CLI login, logout, account selection, tenant selection, or subscription selection. + +### Command contract + +Extend `fab auth` with: + +```text +fab auth login --source azure-cli [--tenant ] [--no-prompt] +fab auth status [--check] [--audience fabric|storage|azure|powerbi] +``` + +Rules: + +- Azure CLI source flags conflict with direct source flags. +- Unattended Azure CLI login requires `--tenant` and `--no-prompt`. +- Bare attended login and eligible progressive first use share the existing chooser. +- Progressive discovery offers Azure CLI only for a supported user identity. +- Workload identities require explicit login in the first release. +- Default chooser action is defer; defer returns `AuthenticationRequired` and exit 4 without writing state. +- Cancellation returns exit 2 without writing state. + +### Persistent state + +Evolve `auth.json` to a versioned source record: + +```json +{ + "version": 2, + "source": "azure-cli", + "cloud": "AzureCloud", + "tenant_id": "", + "principal_id": "", + "principal_type": "user", + "account": "", + "subscription_id": null, + "subscription_name": null, + "app_id": "", + "bound_at": "" +} +``` + +Legacy records migrate idempotently to source `fabric-cli` without deleting the MSAL cache. Candidate source replacement follows validate-then-commit semantics: + +1. Validate Fabric readiness and identity. +2. Acquire the interprocess state lock. +3. Recheck the current state. +4. Atomically replace `auth.json`. +5. Preserve unrelated configuration and the prior state if any step fails. + +The configuration directory remains owner-only (`0700`) and auth state remains owner-only (`0600`). Runtime environment overrides never modify persistent state. + +### Identity binding + +The binding pins cloud, canonical tenant ID, stable principal ID, and principal type. Subscription metadata is display-only and nullable. Every fresh token must match the binding before a service request. + +The implementation mechanism for establishing stable tenant and principal identity is blocked on security decision Q4. If token-derived validation is approved, it must verify issuer, signature, audience, expiration, `tid`, and `oid`; otherwise the provider must use the approved metadata contract. Missing stable identity fails closed. + +### Status and logout + +Plain `fab auth status` remains exit 0 and becomes passive: no provider call, no Azure CLI process, and readiness `unknown` when local state is insufficient. + +`fab auth status --check --audience ` performs one active check and returns exit 0 when ready or exit 4 for readiness failures. Text output retains the current leading status line and legacy field order. Structured output retains the current envelope and legacy token keys with `"N/A"` during the deprecation window. + +Logout clears only Fabric-owned state for the configured source: + +- Azure CLI source: binding and in-process token cache. +- Direct source: current Fabric CLI auth state and its MSAL cache. +- All sources: Fabric CLI memory and context caches as required. + +Unrelated CLI configuration and Azure CLI state remain unchanged. + +### Errors and output + +Add structured error definitions through `fabric_cli.errors` and constants through `fab_constant`; do not hardcode user-facing messages in handlers. Readiness errors map to exit 4, usage/conflict/cancellation errors map to exit 2, and unexpected errors remain exit 1. + +All output uses existing `fab_ui` text and JSON renderers. JSON stdout contains one document; prompts and diagnostics use the diagnostic stream. Logs and telemetry exclude tokens, claims challenges, process output, command arguments, and identity identifiers. + +### HTTP, SDK, deploy, and user capability integration + +- `fab_api_client.do_request` requests tokens from the effective provider without allowing interaction or replay. +- Generalize `create_fabric_token_credential` behind its existing public factory so `fabric-cicd` receives a headless credential for the effective source. +- Run Fabric readiness preflight before entering deploy's catch-all so readiness failures preserve exit 4 and are not wrapped as `DeploymentFailed`. +- Replace source-specific `identity_type == "user"` checks with principal-capability checks for browser-open and personal-workspace behavior. + +## Dependencies + +- `azure-identity` for `AzureCliCredential`. +- Existing `azure-core`, MSAL, secure file utilities, output renderers, and command parser infrastructure. +- Security ownership and approval for executable resolution, principal binding, state, errors, and telemetry. +- Fabric agent-experience decision for the attended-host interaction channel. +- Fabric CLI engineering decision for the feature flag and rollout rings. + +## Rollout and Compatibility + +- Gate Azure CLI source selection and progressive offers independently where possible. +- Enable explicit unattended login before progressive offers. +- Preserve every existing direct authentication syntax and route mapping. +- Rollback disables Azure CLI selection and offers without destructively rewriting a recoverable source marker. +- Existing scripts consuming status retain legacy token keys as `"N/A"` for one deprecation window. + +## Testing Strategy + +### Unit tests + +- Parser conflicts, source selection, interaction classification, error mapping, and output models. +- Azure CLI executable absence, timeout, sanitized failures, scope allowlist, tenant/principal mismatch, refresh, cache coalescing, and failure non-caching. +- State migration, locking, atomic replacement, permissions, failed replacement preservation, and runtime-only environment precedence. +- Passive status zero-call behavior, active single-audience behavior, and source-local logout. + +### Integration tests + +- Shared execution across command-line, auth, REPL, JSON, pipe, CI, callbacks, and batch paths. +- Exactly-once handler and request continuation after consent. +- HTTP route-to-scope mapping and no request replay. +- Headless SDK bridge and deploy preflight error preservation. +- Existing direct user, service-principal, certificate, federation, managed-identity, and raw-token suites. + +### End-to-end and release tests + +- Windows, Linux, and macOS with Azure CLI installed, absent, signed out, user signed in, workload identity, guest tenant, and no subscription. +- Identity drift, Azure CLI context race, timeout, refresh, concurrency, and claims challenge. +- Representative Fabric Skills runs using a prepared Azure CLI identity without a second interactive login. +- Feature-gate enablement, disablement, and rollback. + +The parent test plan remains the acceptance evidence ledger for all 59 requirements and 40 mapped tests. + +## Open Gates + +| Gate | Owner | Blocks | +| --- | --- | --- | +| Q2: Select feature flag and rollout rings | Fabric CLI engineering | Rollout implementation | +| Q3: Assign final security approval owner | Fabric security and Fabric CLI leadership | Design lock | +| Q4: Approve principal identity validation mechanism | Fabric security | Binding implementation | +| Q5: Select attended-host interaction channel | Fabric agent experience and Fabric CLI | Attended-agent release | + +## Implementation Plan + +See [implementation-plan.md](implementation-plan.md) for the sequenced workstreams, dependencies, and proposed ADO task breakdown. diff --git a/Features/1694265/implementation-plan.md b/Features/1694265/implementation-plan.md new file mode 100644 index 00000000..74fcaebb --- /dev/null +++ b/Features/1694265/implementation-plan.md @@ -0,0 +1,71 @@ +# Implementation Plan — Feature 1694265 (fabric-cli) + +This plan decomposes the repo-specific [design spec](design-spec.md) into independently reviewable workstreams. The parent ADO item is User Story 1694265. + +## Delivery Principles + +- Land policy and compatibility tests before changing provider behavior. +- Keep all providers noninteractive below the coordinator. +- Preserve existing direct authentication while adding Azure CLI. +- Do not merge slices that depend on unresolved security or host-integration gates. +- Track requirement and test-plan evidence from the first implementation pull request. + +## Proposed Task Breakdown + +| Slice | Proposed ADO Task | Implementation scope | Primary files | Depends on | Exit criteria | +| ---: | --- | --- | --- | --- | --- | +| 1 | Add shared executor, authentication coordinator, and interaction policy | Unify command-line, auth, REPL, and batch dispatch; classify interaction; resolve effective source; map readiness exits; guarantee exactly-once handler execution | `main.py`, `core/fab_interactive.py`, new coordinator/executor modules, `core/fab_decorators.py` | None | All execution surfaces use one policy; local commands bypass auth; unattended commands do not prompt; readiness errors exit 4 | +| 2 | Add Azure CLI provider and explicit login contract | Add `azure-identity`; parser flags and conflicts; provider protocol; trusted executable behavior; timeout; sanitization; one-scope allowlist | `pyproject.toml`, `parsers/fab_auth_parser.py`, `commands/auth/fab_auth.py`, new provider modules, `errors/auth.py` | Slice 1 interface alignment | Explicit attended and unattended login paths validate Fabric without invoking Azure CLI login or accepting arbitrary scopes | +| 3 | Implement identity binding and atomic auth state | Versioned source state; legacy migration; runtime-only environment override; locking; atomic writes; permissions; pinned identity; process cache and concurrency | `core/fab_auth.py`, `core/fab_state_config.py`, `utils/fab_secure_io.py`, new state/cache modules | Slices 1-2; Q4 for principal validation | Failed replacement preserves prior state; fresh tokens match binding; no delegated token reaches disk | +| 4 | Implement shared chooser and exactly-once continuation | Progressive eligible-user discovery; default defer; alternate direct options; direct-terminal chooser; attended-host abstraction; separate-login fallback | Coordinator, `commands/auth/fab_auth.py`, `utils/fab_ui.py`, host interaction abstraction | Slices 1 and 3; Q5 for host transport | Default Enter defers; cancellation and defer write no state; successful consent invokes one handler/request | +| 5 | Implement passive/active status, stable errors, and source-local logout | Passive status; `--check`; audience selection; compatibility output; all stable errors; stream separation; capability checks; scoped logout | Auth parser/commands, `core/fab_constant.py`, `errors/auth.py`, output models, `commands/fs/fab_fs_open.py` | Slices 1-3 | Passive status makes zero provider calls; active status checks one audience; logout preserves unrelated and Azure CLI state | +| 6 | Integrate HTTP, SDK bridge, deploy, and batch paths | Source-aware request acquisition; headless credential factory; deploy preflight before catch-all; claims behavior; fail-fast batch counts; Power BI route compatibility | `client/fab_api_client.py`, `core/fab_msal_bridge.py`, deploy command, `main.py` | Slices 1-5 | No replay; SDK callbacks stay headless; deploy readiness exits 4; Power BI remains mapped to Fabric scope | +| 7 | Complete release matrix, docs, telemetry, and Skills pilot | Full regression matrix; telemetry safety; docs/examples; feature gates; rollout and rollback evidence; representative Skills runs | Tests, docs, telemetry integration, release configuration | Slices 1-6; Q2-Q3 | All parent test-plan rows have evidence; direct sources regress cleanly; rollout and rollback are approved | + +## Dependency Graph + +```text +Slice 1 ──┬──> Slice 2 ──> Slice 3 ──┬──> Slice 4 ──┐ + │ └──> Slice 5 ──┼──> Slice 6 ──> Slice 7 + └─────────────────────────────────────────┘ + +Q4 gates identity validation in Slice 3. +Q5 gates attended-host integration in Slice 4. +Q2 and Q3 gate rollout completion in Slice 7. +``` + +## Pull Request Sequence + +1. **Policy and compatibility harness:** interaction classifier, executor contract, current-behavior regression tests, and stable error categories. +2. **Provider foundation:** provider protocol, Azure CLI parser contract, allowlist, timeout, sanitization, and mocked provider tests. +3. **State and binding:** versioned migration, runtime override model, locking, atomic write, cache, and approved principal validation. +4. **User interaction:** direct chooser and continuation first; attended-host transport only after Q5. +5. **Status and lifecycle:** passive/active status, compatibility output, capability checks, and source-local logout. +6. **Integration surfaces:** HTTP, SDK bridge, deploy, batch, claims, and Power BI route regression. +7. **Release hardening:** platform matrix, docs, telemetry, Skills pilot, flags, and rollback. + +Each pull request should link to its ADO task and update the requirement/test evidence ledger. + +## Validation Matrix + +| Layer | Required coverage | +| --- | --- | +| Parser | New flags, conflicts, required tenant/no-prompt combinations, audience allowlist | +| Policy | Direct terminal, approved attended host, undeclared host, JSON, pipe, batch, CI, callback, status, logout | +| Provider | Installed/signed-in states, timeout, sanitized error, audience mapping, refresh, concurrency, drift | +| State | Migration, permissions, atomicity, lock contention, failed replacement, environment precedence | +| Execution | CLI, auth commands, REPL, batch fail-fast, exactly-once continuation | +| Integration | HTTP routes, SDK credential, deploy preflight, user capabilities, claims handling | +| Regression | MSAL user, SPN secret/certificate/federation, managed identity, raw tokens | +| Release | Windows/Linux/macOS, guest/no-subscription, Skills pilot, feature gates, rollback | + +## Task Creation Readiness + +Before creating ADO work items: + +- The Feature Registry artifacts must be present on its default branch. +- This repo's `Features/1694265/design-spec.md` must be reviewed and merged to the code repo's default branch. +- The ADO organization, project, hierarchy, area path, iteration, and owners must be confirmed. +- Existing closed Task 1728448 remains decision history and is not reused. + +After the readiness gate passes, create the seven work items through the `manage-tasks` workflow so each ADO ID is used to generate its local `task-.md` file. diff --git a/Features/1694265/registry.md b/Features/1694265/registry.md new file mode 100644 index 00000000..68458234 --- /dev/null +++ b/Features/1694265/registry.md @@ -0,0 +1,20 @@ +# Feature 1694265 — Registry Pointer + +This folder contains area-specific artifacts for Feature 1694265 in the **fabric-cli** repo. + +## Feature Registry (Parent) + +- **Registry Repo:** [fabric-cli](https://powerbi@dev.azure.com/powerbi/Trident/_git/PlatformDevFeatureRegistry) +- **Feature Folder:** `Features/active/1694265/` (moves to `Features/done/1694265/` when archived) +- **ADO Work Item:** [#1694265](https://powerbi@dev.azure.com/powerbi/Trident/_workitems/edit/1694265) *(stable canonical link)* + +> **Note:** The Feature Registry uses an `active/done` folder hierarchy. After a feature is archived, the folder path changes from `Features/active/` to `Features/done/`. Use the ADO Work Item link above as the stable reference. + +## Navigation + +> **Tip:** These links point to the `active/` path and will break after archival. Use the ADO Work Item link above to find the feature regardless of its current folder. + +- [Requirements Spec](https://powerbi@dev.azure.com/powerbi/Trident/_git/PlatformDevFeatureRegistry?path=/Features/active/1694265/requirements-spec.md) (in Feature Registry — active only) +- [Engineering Design](https://powerbi@dev.azure.com/powerbi/Trident/_git/PlatformDevFeatureRegistry?path=/Features/active/1694265/engineering-design.md) (in Feature Registry — active only) +- [Implementation Handoff](https://powerbi@dev.azure.com/powerbi/Trident/_git/PlatformDevFeatureRegistry?path=/Features/active/1694265/implementation-handoff.md) (in Feature Registry — active only) +- [Test Plan](https://powerbi@dev.azure.com/powerbi/Trident/_git/PlatformDevFeatureRegistry?path=/Features/active/1694265/test-plan.md) (in Feature Registry — active only) From 8030fccf525774937d848142e1ae18b450786a0d Mon Sep 17 00:00:00 2001 From: ohadedry <68539765+ohadedry@users.noreply.github.com> Date: Mon, 10 Aug 2026 12:52:05 +0300 Subject: [PATCH 02/15] chore: update release notes for next release (#270) --- .changes/unreleased/fixed-20260706-165104.yaml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .changes/unreleased/fixed-20260706-165104.yaml diff --git a/.changes/unreleased/fixed-20260706-165104.yaml b/.changes/unreleased/fixed-20260706-165104.yaml deleted file mode 100644 index 30afdd7c..00000000 --- a/.changes/unreleased/fixed-20260706-165104.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: fixed -body: Remove invalid 'host-app/' prefix from User-Agent host app suffix -time: 2026-07-06T16:51:04+03:00 -custom: - Author: KupoCat - AuthorLink: https://github.com/KupoCat From e8a91cdc8ca6f6dada983757b28f1004f202e569 Mon Sep 17 00:00:00 2001 From: aviatco <32952699+aviatco@users.noreply.github.com> Date: Tue, 11 Aug 2026 16:29:01 +0300 Subject: [PATCH 03/15] fix: add host app when running deploy cmd (#269) Co-authored-by: Aviat Cohen Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- pyproject.toml | 2 +- .../fs/deploy/fab_fs_deploy_config_file.py | 5 ++ tests/test_commands/test_deploy.py | 68 ++++++++++++++++++- .../test_fab_deploy_bulk_publish.py | 1 + 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bcda6e7c..dee33a6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ "psutil==7.0.0", "requests", "cryptography", - "fabric-cicd>=1.2.0", + "fabric-cicd>=1.3.0", ] [project.scripts] diff --git a/src/fabric_cli/commands/fs/deploy/fab_fs_deploy_config_file.py b/src/fabric_cli/commands/fs/deploy/fab_fs_deploy_config_file.py index 1a8e3fbc..655c2259 100644 --- a/src/fabric_cli/commands/fs/deploy/fab_fs_deploy_config_file.py +++ b/src/fabric_cli/commands/fs/deploy/fab_fs_deploy_config_file.py @@ -42,6 +42,11 @@ def deploy_with_config_file(args: Namespace) -> None: except json.JSONDecodeError: # If it's not a valid JSON string, keep it as is pass + + deploy_parameters["host_app"] = ( + f"{fab_constant.API_USER_AGENT}/{fab_constant.FAB_VERSION}" + ) + result = deploy_with_config( config_file_path=deploy_config_file, environment=args.target_env, diff --git a/tests/test_commands/test_deploy.py b/tests/test_commands/test_deploy.py index 2b8e9e8f..4697548d 100644 --- a/tests/test_commands/test_deploy.py +++ b/tests/test_commands/test_deploy.py @@ -3,7 +3,8 @@ import os import platform -from unittest.mock import patch +from argparse import Namespace +from unittest.mock import MagicMock, patch import pytest import yaml @@ -354,3 +355,68 @@ def test_deploy_with_home_directory_path_success( mock_print_done.assert_called() assert "Deployment completed successfully" in str( mock_print_done.call_args) + + def _run_deploy_with_config_file(self, deploy_with_config, params=None): + """Invoke deploy_with_config_file with fabric-cicd symbols patched (no network).""" + from fabric_cli.commands.fs.deploy import ( + fab_fs_deploy_config_file as deploy_mod, + ) + + args = Namespace( + config="config.yml", + target_env="dev", + command_path="deploy", + params=params if params is not None else [], + ) + + with ( + patch.object(deploy_mod, "deploy_with_config", deploy_with_config), + patch.object( + deploy_mod, "create_fabric_token_credential", MagicMock()), + patch.object(deploy_mod, "append_feature_flag", MagicMock()), + patch.object(deploy_mod, "disable_file_logging", MagicMock()), + patch.object( + deploy_mod, "configure_external_file_logging", MagicMock()), + patch.object( + deploy_mod.fab_state_config, "get_config", return_value="false" + ), + patch.object(deploy_mod.fab_ui, + "print_output_format", MagicMock()), + ): + deploy_mod.deploy_with_config_file(args) + + def _capture_deploy_host_app(self, params=None): + """Run deploy_with_config_file and return the host_app passed to fabric-cicd.""" + captured = {} + + def fake_deploy_with_config( + *, + config_file_path, + token_credential, + environment="N/A", + config_override=None, + host_app=None, + ): + captured["host_app"] = host_app + return MagicMock(message="Deployment completed successfully") + + self._run_deploy_with_config_file( + fake_deploy_with_config, params=params) + + return captured["host_app"] + + def test_deploy_passes_host_app_success(self): + """CLI passes host_app as 'ms-fabric-cli/'.""" + from fabric_cli.core import fab_constant + + host_app = self._capture_deploy_host_app() + + assert host_app == f"{fab_constant.API_USER_AGENT}/{fab_constant.FAB_VERSION}" + + def test_deploy_host_app_cannot_be_spoofed_via_params_success(self): + """A user-supplied host_app (via -P) is overridden by the CLI-controlled value.""" + from fabric_cli.core import fab_constant + + host_app = self._capture_deploy_host_app(params=["host_app=spoofed"]) + + assert host_app == f"{fab_constant.API_USER_AGENT}/{fab_constant.FAB_VERSION}" diff --git a/tests/test_utils/test_fab_deploy_bulk_publish.py b/tests/test_utils/test_fab_deploy_bulk_publish.py index 0886554d..fed1be5b 100644 --- a/tests/test_utils/test_fab_deploy_bulk_publish.py +++ b/tests/test_utils/test_fab_deploy_bulk_publish.py @@ -27,6 +27,7 @@ def _run_deploy(self, tmp_path, bulk_publish, mock_fab_set_state_config): target_env="dev", params=None, bulk_publish=bulk_publish, + command_path="deploy", ) with ( From 21d3cd3fd1bb95211a7b352f67626daa9e5b9f92 Mon Sep 17 00:00:00 2001 From: ohadedry <68539765+ohadedry@users.noreply.github.com> Date: Mon, 17 Aug 2026 11:17:27 +0300 Subject: [PATCH 04/15] fix: clean bulk_publish flags when deploy complete (#272) --- requirements-dev.txt | 2 +- .../fs/deploy/fab_fs_deploy_config_file.py | 30 +++++-- .../test_fab_deploy_bulk_publish.py | 81 +++++++++++++++---- 3 files changed, 90 insertions(+), 23 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 23e909b6..342111a3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ argcomplete>=3.6.2 psutil==7.0.0 requests cryptography -fabric-cicd>=1.2.0 +fabric-cicd>=1.3.0 # Testing and Building Requirements tox>=4.20.0 diff --git a/src/fabric_cli/commands/fs/deploy/fab_fs_deploy_config_file.py b/src/fabric_cli/commands/fs/deploy/fab_fs_deploy_config_file.py index 655c2259..e5e35433 100644 --- a/src/fabric_cli/commands/fs/deploy/fab_fs_deploy_config_file.py +++ b/src/fabric_cli/commands/fs/deploy/fab_fs_deploy_config_file.py @@ -4,7 +4,13 @@ import json from argparse import Namespace -from fabric_cicd import append_feature_flag, configure_external_file_logging, deploy_with_config, disable_file_logging # type: ignore +from fabric_cicd import ( # type: ignore + append_feature_flag, + configure_external_file_logging, + deploy_with_config, + disable_file_logging, + remove_feature_flag, +) from fabric_cli.core import fab_constant, fab_state_config from fabric_cli.core import fab_logger @@ -37,8 +43,7 @@ def deploy_with_config_file(args: Namespace) -> None: for param in deploy_parameters: if isinstance(deploy_parameters[param], str): try: - deploy_parameters[param] = json.loads( - deploy_parameters[param]) + deploy_parameters[param] = json.loads(deploy_parameters[param]) except json.JSONDecodeError: # If it's not a valid JSON string, keep it as is pass @@ -51,17 +56,20 @@ def deploy_with_config_file(args: Namespace) -> None: config_file_path=deploy_config_file, environment=args.target_env, token_credential=create_fabric_token_credential(), # MSAL bridge TokenCredential - **deploy_parameters + **deploy_parameters, ) if result: - fab_ui.print_output_format( - args, message=result.message) + fab_ui.print_output_format(args, message=result.message) except Exception as e: raise FabricCLIError( - f"Deployment failed: {str(e)}", - fab_constant.ERROR_IN_DEPLOYMENT) + f"Deployment failed: {str(e)}", fab_constant.ERROR_IN_DEPLOYMENT + ) + finally: + bulk_publish_enabled = getattr(args, "bulk_publish", False) + if bulk_publish_enabled: + _remove_bulk_publish_feature_flags() def _apply_bulk_publish_feature_flags(args: Namespace) -> None: @@ -82,3 +90,9 @@ def _apply_bulk_publish_feature_flags(args: Namespace) -> None: "fabric-cicd and may change or fail; omit the '--bulk_publish' flag " "to use standard per-item publish." ) + + +def _remove_bulk_publish_feature_flags() -> None: + """Remove command-scoped bulk publish flags from fabric-cicd global state.""" + remove_feature_flag("enable_experimental_features") + remove_feature_flag("enable_bulk_publish") diff --git a/tests/test_utils/test_fab_deploy_bulk_publish.py b/tests/test_utils/test_fab_deploy_bulk_publish.py index fed1be5b..d241fd89 100644 --- a/tests/test_utils/test_fab_deploy_bulk_publish.py +++ b/tests/test_utils/test_fab_deploy_bulk_publish.py @@ -3,6 +3,10 @@ from unittest.mock import patch +import pytest + +from fabric_cli.core.fab_exceptions import FabricCLIError + class TestDeployBulkPublish: """ @@ -11,18 +15,11 @@ class TestDeployBulkPublish: HTTP cassettes. """ - def _run_deploy(self, tmp_path, bulk_publish, mock_fab_set_state_config): - """Invoke deploy_with_config_file with fabric-cicd mocked, returning the - append_feature_flag mock for assertions.""" + def _create_deploy_args(self, tmp_path, bulk_publish): + """Create arguments for deploy_with_config_file.""" from argparse import Namespace - import fabric_cli.commands.fs.deploy.fab_fs_deploy_config_file as deploy_mod - from fabric_cli.core import fab_constant - - # disable debug mode so fabric-cicd file logging is disabled during the run - mock_fab_set_state_config(fab_constant.FAB_DEBUG_ENABLED, "false") - - args = Namespace( + return Namespace( config=str(tmp_path / "config.yml"), target_env="dev", params=None, @@ -30,8 +27,18 @@ def _run_deploy(self, tmp_path, bulk_publish, mock_fab_set_state_config): command_path="deploy", ) + def _run_deploy_success(self, tmp_path, bulk_publish, mock_fab_set_state_config): + """Run a successful deployment and return feature flag mocks.""" + import fabric_cli.commands.fs.deploy.fab_fs_deploy_config_file as deploy_mod + from fabric_cli.core import fab_constant + + # disable debug mode so fabric-cicd file logging is disabled during the run + mock_fab_set_state_config(fab_constant.FAB_DEBUG_ENABLED, "false") + args = self._create_deploy_args(tmp_path, bulk_publish) + with ( patch.object(deploy_mod, "append_feature_flag") as mock_flag, + patch.object(deploy_mod, "remove_feature_flag") as mock_remove_flag, patch.object(deploy_mod, "deploy_with_config", return_value=None), patch.object(deploy_mod, "disable_file_logging"), patch.object(deploy_mod, "configure_external_file_logging"), @@ -41,28 +48,74 @@ def _run_deploy(self, tmp_path, bulk_publish, mock_fab_set_state_config): ): deploy_mod.deploy_with_config_file(args) - return mock_flag + return mock_flag, mock_remove_flag + + def _run_deploy_failure(self, tmp_path, bulk_publish, mock_fab_set_state_config): + """Run a failed deployment and return feature flag mocks.""" + import fabric_cli.commands.fs.deploy.fab_fs_deploy_config_file as deploy_mod + from fabric_cli.core import fab_constant + + # disable debug mode so fabric-cicd file logging is disabled during the run + mock_fab_set_state_config(fab_constant.FAB_DEBUG_ENABLED, "false") + args = self._create_deploy_args(tmp_path, bulk_publish) + + with ( + patch.object(deploy_mod, "append_feature_flag") as mock_flag, + patch.object(deploy_mod, "remove_feature_flag") as mock_remove_flag, + patch.object( + deploy_mod, + "deploy_with_config", + side_effect=Exception("Simulated deployment failure"), + ), + patch.object(deploy_mod, "disable_file_logging"), + patch.object(deploy_mod, "configure_external_file_logging"), + patch.object( + deploy_mod, "create_fabric_token_credential", return_value=None + ), + ): + with pytest.raises(FabricCLIError): + deploy_mod.deploy_with_config_file(args) + + return mock_flag, mock_remove_flag def test_deploy_bulk_publish_enabled_appends_experimental_flags_success( self, tmp_path, mock_fab_set_state_config ): """When --bulk_publish is set, both experimental bulk publish flags are appended.""" - mock_flag = self._run_deploy(tmp_path, True, mock_fab_set_state_config) + mock_flag, mock_remove_flag = self._run_deploy_success( + tmp_path, True, mock_fab_set_state_config + ) appended = [call.args[0] for call in mock_flag.call_args_list] assert "enable_experimental_features" in appended assert "enable_bulk_publish" in appended # existing behavior is preserved assert "disable_print_identity" in appended + removed = [call.args[0] for call in mock_remove_flag.call_args_list] + assert removed == ["enable_experimental_features", "enable_bulk_publish"] + + def test_deploy_bulk_publish_enabled_removes_flags_on_failure( + self, tmp_path, mock_fab_set_state_config + ): + """Bulk publish flags are removed when deployment raises an exception.""" + _, mock_remove_flag = self._run_deploy_failure( + tmp_path, True, mock_fab_set_state_config + ) + + removed = [call.args[0] for call in mock_remove_flag.call_args_list] + assert removed == ["enable_experimental_features", "enable_bulk_publish"] def test_deploy_bulk_publish_disabled_by_default_omits_flags_success( self, tmp_path, mock_fab_set_state_config ): - """When --bulk_publish is not set (default), bulk publish flags are not appended.""" - mock_flag = self._run_deploy(tmp_path, False, mock_fab_set_state_config) + """When --bulk_publish is not set, bulk publish flags are not changed.""" + mock_flag, mock_remove_flag = self._run_deploy_success( + tmp_path, False, mock_fab_set_state_config + ) appended = [call.args[0] for call in mock_flag.call_args_list] assert "enable_experimental_features" not in appended assert "enable_bulk_publish" not in appended # existing behavior is preserved assert "disable_print_identity" in appended + mock_remove_flag.assert_not_called() From 657b3415cf0cb188111efe27dbcfab24ef7bf80a Mon Sep 17 00:00:00 2001 From: ohadedry <68539765+ohadedry@users.noreply.github.com> Date: Wed, 19 Aug 2026 09:45:36 +0300 Subject: [PATCH 05/15] chore(release): v1.7.0 (#273) --- .../unreleased/added-20260417-105920.yaml | 6 ----- .../unreleased/added-20260512-150558.yaml | 6 ----- .../unreleased/added-20260625-145755.yaml | 6 ----- .../unreleased/added-20260701-121400.yaml | 6 ----- .../unreleased/added-20260706-091126.yaml | 6 ----- .../unreleased/fixed-20260427-142055.yaml | 6 ----- .../unreleased/fixed-20260607-134043.yaml | 6 ----- .../unreleased/fixed-20260607-135834.yaml | 6 ----- .../unreleased/fixed-20260618-123845.yaml | 6 ----- .../unreleased/fixed-20260716-092645.yaml | 6 ----- .../unreleased/new-items-20260513-105250.yaml | 6 ----- .changes/v1.7.0.md | 22 +++++++++++++++ docs/release-notes.md | 27 +++++++++++++++++-- src/fabric_cli/__init__.py | 2 +- 14 files changed, 48 insertions(+), 69 deletions(-) delete mode 100644 .changes/unreleased/added-20260417-105920.yaml delete mode 100644 .changes/unreleased/added-20260512-150558.yaml delete mode 100644 .changes/unreleased/added-20260625-145755.yaml delete mode 100644 .changes/unreleased/added-20260701-121400.yaml delete mode 100644 .changes/unreleased/added-20260706-091126.yaml delete mode 100644 .changes/unreleased/fixed-20260427-142055.yaml delete mode 100644 .changes/unreleased/fixed-20260607-134043.yaml delete mode 100644 .changes/unreleased/fixed-20260607-135834.yaml delete mode 100644 .changes/unreleased/fixed-20260618-123845.yaml delete mode 100644 .changes/unreleased/fixed-20260716-092645.yaml delete mode 100644 .changes/unreleased/new-items-20260513-105250.yaml create mode 100644 .changes/v1.7.0.md diff --git a/.changes/unreleased/added-20260417-105920.yaml b/.changes/unreleased/added-20260417-105920.yaml deleted file mode 100644 index e5bee1bf..00000000 --- a/.changes/unreleased/added-20260417-105920.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: added -body: Adds support for environment definitions -time: 2026-04-17T10:59:20.288002+03:00 -custom: - Author: v-alexmoraru - AuthorLink: https://github.com/v-alexmoraru diff --git a/.changes/unreleased/added-20260512-150558.yaml b/.changes/unreleased/added-20260512-150558.yaml deleted file mode 100644 index b3e8c084..00000000 --- a/.changes/unreleased/added-20260512-150558.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: added -body: Return the job instance ID as a structured field in the JSON output of ``job run`` and ``job start`` commands -time: 2026-05-12T15:05:58.3242491+03:00 -custom: - Author: shirasassoon - AuthorLink: https://github.com/shirasassoon diff --git a/.changes/unreleased/added-20260625-145755.yaml b/.changes/unreleased/added-20260625-145755.yaml deleted file mode 100644 index 768759c2..00000000 --- a/.changes/unreleased/added-20260625-145755.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: added -body: Support creating SQLDatabase with optional parameters. Support `creationMode` of type `New`, `Restore` and `RestoreDeletedDatabase`. -time: 2026-06-25T14:57:55.03150683Z -custom: - Author: aviatco - AuthorLink: https://github.com/aviatco diff --git a/.changes/unreleased/added-20260701-121400.yaml b/.changes/unreleased/added-20260701-121400.yaml deleted file mode 100644 index 4a7280eb..00000000 --- a/.changes/unreleased/added-20260701-121400.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: added -body: Adds the `--bulk_publish` flag to the `deploy` command to opt into experimental bulk publish -time: 2026-07-01T12:14:00.000000+00:00 -custom: - Author: ayeshurun - AuthorLink: https://github.com/ayeshurun diff --git a/.changes/unreleased/added-20260706-091126.yaml b/.changes/unreleased/added-20260706-091126.yaml deleted file mode 100644 index 46352c5c..00000000 --- a/.changes/unreleased/added-20260706-091126.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: added -body: Add new 'bulk-export' command for exporting workspace or folder items -time: 2026-07-06T09:11:26.916814199Z -custom: - Author: ohadedry - AuthorLink: https://github.com/ohadedry diff --git a/.changes/unreleased/fixed-20260427-142055.yaml b/.changes/unreleased/fixed-20260427-142055.yaml deleted file mode 100644 index fe7b1e8f..00000000 --- a/.changes/unreleased/fixed-20260427-142055.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: fixed -body: Wraps long text on multiple lines -time: 2026-04-27T14:20:55.3215682+03:00 -custom: - Author: v-alexmoraru - AuthorLink: https://github.com/v-alexmoraru diff --git a/.changes/unreleased/fixed-20260607-134043.yaml b/.changes/unreleased/fixed-20260607-134043.yaml deleted file mode 100644 index e3d83b44..00000000 --- a/.changes/unreleased/fixed-20260607-134043.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: fixed -body: Restrict file and directory permissions on auth, config, context, and log paths to prevent local credential exposure on multi-user systems -time: 2026-06-07T13:40:43+02:00 -custom: - Author: iemejia - AuthorLink: https://github.com/iemejia diff --git a/.changes/unreleased/fixed-20260607-135834.yaml b/.changes/unreleased/fixed-20260607-135834.yaml deleted file mode 100644 index 34284f53..00000000 --- a/.changes/unreleased/fixed-20260607-135834.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: fixed -body: Clean up temporary file after table maintenance job execution -time: 2026-06-07T13:58:34+02:00 -custom: - Author: iemejia - AuthorLink: https://github.com/iemejia diff --git a/.changes/unreleased/fixed-20260618-123845.yaml b/.changes/unreleased/fixed-20260618-123845.yaml deleted file mode 100644 index 515ba816..00000000 --- a/.changes/unreleased/fixed-20260618-123845.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: fixed -body: Enforce owner-only permissions on local files on POSIX -time: 2026-06-18T12:38:45Z -custom: - Author: aviatcohen - AuthorLink: https://github.com/aviatcohen diff --git a/.changes/unreleased/fixed-20260716-092645.yaml b/.changes/unreleased/fixed-20260716-092645.yaml deleted file mode 100644 index c456bf24..00000000 --- a/.changes/unreleased/fixed-20260716-092645.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: fixed -body: Handle HTTP 429 responses missing the Retry-After header instead of failing with an unexpected error -time: 2026-07-16T09:26:45+03:00 -custom: - Author: ayeshurun - AuthorLink: https://github.com/ayeshurun diff --git a/.changes/unreleased/new-items-20260513-105250.yaml b/.changes/unreleased/new-items-20260513-105250.yaml deleted file mode 100644 index f04b0767..00000000 --- a/.changes/unreleased/new-items-20260513-105250.yaml +++ /dev/null @@ -1,6 +0,0 @@ -kind: new-items -body: Supports Digital Twin Builder Flow item -time: 2026-05-13T10:52:50.730594+03:00 -custom: - Author: v-alexmoraru - AuthorLink: https://github.com/v-alexmoraru diff --git a/.changes/v1.7.0.md b/.changes/v1.7.0.md new file mode 100644 index 00000000..457b7ffe --- /dev/null +++ b/.changes/v1.7.0.md @@ -0,0 +1,22 @@ +## [v1.7.0](https://pypi.org/project/ms-fabric-cli/v1.7.0) - August 18, 2026 + +### 🆕 New Items Support + +* Supports Digital Twin Builder Flow item by [v-alexmoraru](https://github.com/v-alexmoraru) + +### ✨ New Functionality + +* Adds support for environment definitions by [v-alexmoraru](https://github.com/v-alexmoraru) +* Return the job instance ID as a structured field in the JSON output of ``job run`` and ``job start`` commands by [shirasassoon](https://github.com/shirasassoon) +* Support creating SQLDatabase with optional parameters. Support `creationMode` of type `New`, `Restore` and `RestoreDeletedDatabase`. by [aviatco](https://github.com/aviatco) +* Adds the `--bulk_publish` flag to the `deploy` command to opt into experimental bulk publish by [ayeshurun](https://github.com/ayeshurun) +* Add new 'bulk-export' command for exporting workspace or folder items by [ohadedry](https://github.com/ohadedry) + +### 🔧 Bug Fix + +* Wraps long text on multiple lines by [v-alexmoraru](https://github.com/v-alexmoraru) +* Restrict file and directory permissions on auth, config, context, and log paths to prevent local credential exposure on multi-user systems by [iemejia](https://github.com/iemejia) +* Clean up temporary file after table maintenance job execution by [iemejia](https://github.com/iemejia) +* Enforce owner-only permissions on local files on POSIX by [aviatcohen](https://github.com/aviatcohen) +* Handle HTTP 429 responses missing the Retry-After header instead of failing with an unexpected error by [ayeshurun](https://github.com/ayeshurun) + diff --git a/docs/release-notes.md b/docs/release-notes.md index 1ee24bac..818645bc 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,29 @@ hide: # Release Notes +## [v1.7.0](https://pypi.org/project/ms-fabric-cli/v1.7.0) - August 18, 2026 + +### 🆕 New Items Support + +* Supports Digital Twin Builder Flow item by [v-alexmoraru](https://github.com/v-alexmoraru) + +### ✨ New Functionality + +* Adds support for environment definitions by [v-alexmoraru](https://github.com/v-alexmoraru) +* Return the job instance ID as a structured field in the JSON output of ``job run`` and ``job start`` commands by [shirasassoon](https://github.com/shirasassoon) +* Support creating SQLDatabase with optional parameters. Support `creationMode` of type `New`, `Restore` and `RestoreDeletedDatabase`. by [aviatco](https://github.com/aviatco) +* Adds the `--bulk_publish` flag to the `deploy` command to opt into experimental bulk publish by [ayeshurun](https://github.com/ayeshurun) +* Add new 'bulk-export' command for exporting workspace or folder items by [ohadedry](https://github.com/ohadedry) + +### 🔧 Bug Fix + +* Wraps long text on multiple lines by [v-alexmoraru](https://github.com/v-alexmoraru) +* Restrict file and directory permissions on auth, config, context, and log paths to prevent local credential exposure on multi-user systems by [iemejia](https://github.com/iemejia) +* Clean up temporary file after table maintenance job execution by [iemejia](https://github.com/iemejia) +* Enforce owner-only permissions on local files on POSIX by [aviatcohen](https://github.com/aviatcohen) +* Handle HTTP 429 responses missing the Retry-After header instead of failing with an unexpected error by [ayeshurun](https://github.com/ayeshurun) + + ## [v1.6.1](https://pypi.org/project/ms-fabric-cli/v1.6.1) - April 29, 2026 ### 🆕 New Items Support @@ -16,13 +39,13 @@ hide: ### ✨ New Functionality * Add new `fab find` command for searching the OneLake catalog across workspaces by [nschachter](https://github.com/nschachter) -* Promote VariableLibrary from portal-only to full API support, enabling create, get, set, rm, ls, export, import, cp, and mv commands via the Variable Library REST APIs by [itsnotaboutthecell](https://github.com/itsnotaboutthecell) +* Promote VariableLibrary from portal-only to full API support, enabling `create`, `get`, `set`, `rm`, `ls`, `export`, `import`, `cp`, and `mv` commands via the Variable Library REST APIs by [itsnotaboutthecell](https://github.com/itsnotaboutthecell) * adds hard flag to rm command (permanent delete) by [v-alexmoraru](https://github.com/v-alexmoraru) * supports lakehouse import & export by [v-alexmoraru](https://github.com/v-alexmoraru) ### 🔧 Bug Fix -* Fix "caracters" typo in error message by [alonyeshurun](https://github.com/alonyeshurun) +* fix correct "caracters" typo in error message by [alonyeshurun](https://github.com/alonyeshurun) * Remove hardcoded description when create/import items by [aviatco](https://github.com/aviatco) ### ⚡ Additional Optimizations diff --git a/src/fabric_cli/__init__.py b/src/fabric_cli/__init__.py index a8209acb..ad810223 100644 --- a/src/fabric_cli/__init__.py +++ b/src/fabric_cli/__init__.py @@ -2,4 +2,4 @@ # Licensed under the MIT License. # Don't change this -__version__ = "1.6.1" +__version__ = "1.7.0" From 23a184482f740fccb6ef93315fefe0a8b9d20f1f Mon Sep 17 00:00:00 2001 From: ohadedry <68539765+ohadedry@users.noreply.github.com> Date: Wed, 19 Aug 2026 10:21:07 +0300 Subject: [PATCH 06/15] chore(release): revert changes to previous release notes (#274) --- docs/release-notes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/release-notes.md b/docs/release-notes.md index 818645bc..6cd148c8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -39,13 +39,13 @@ hide: ### ✨ New Functionality * Add new `fab find` command for searching the OneLake catalog across workspaces by [nschachter](https://github.com/nschachter) -* Promote VariableLibrary from portal-only to full API support, enabling `create`, `get`, `set`, `rm`, `ls`, `export`, `import`, `cp`, and `mv` commands via the Variable Library REST APIs by [itsnotaboutthecell](https://github.com/itsnotaboutthecell) +* Promote VariableLibrary from portal-only to full API support, enabling create, get, set, rm, ls, export, import, cp, and mv commands via the Variable Library REST APIs by [itsnotaboutthecell](https://github.com/itsnotaboutthecell) * adds hard flag to rm command (permanent delete) by [v-alexmoraru](https://github.com/v-alexmoraru) * supports lakehouse import & export by [v-alexmoraru](https://github.com/v-alexmoraru) ### 🔧 Bug Fix -* fix correct "caracters" typo in error message by [alonyeshurun](https://github.com/alonyeshurun) +* Fix "caracters" typo in error message by [alonyeshurun](https://github.com/alonyeshurun) * Remove hardcoded description when create/import items by [aviatco](https://github.com/aviatco) ### ⚡ Additional Optimizations From dd5d65b9424dac1949243c49b79374054855ce26 Mon Sep 17 00:00:00 2001 From: Dan Fiedler <151573964+danfiedler-msft@users.noreply.github.com> Date: Tue, 25 Aug 2026 05:15:13 -0400 Subject: [PATCH 07/15] chore: Pin GitHub Actions to full-length commit SHAs (#271) --- .github/dependabot.yml | 9 ++++++++ .github/workflows/ai-issue-triage.yml | 12 +++++------ .github/workflows/changelog-existence.yml | 6 +++--- .github/workflows/create-release.yml | 2 +- .github/workflows/documentation.yml | 4 ++-- .github/workflows/fab-build.yml | 24 ++++++++++----------- .github/workflows/semantic-pull-request.yml | 6 +++--- 7 files changed, 36 insertions(+), 27 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f33a02cd..ed214807 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,3 +10,12 @@ updates: directory: "/" schedule: interval: weekly + - package-ecosystem: "github-actions" + directory: "/" + groups: + github-actions: + patterns: ["*"] + schedule: + interval: "weekly" + cooldown: + default-days: 7 diff --git a/.github/workflows/ai-issue-triage.yml b/.github/workflows/ai-issue-triage.yml index b42198f9..f58389b2 100644 --- a/.github/workflows/ai-issue-triage.yml +++ b/.github/workflows/ai-issue-triage.yml @@ -30,11 +30,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - name: Resolve issue details id: issue - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 with: script: | const issueNumber = context.payload.issue?.number || ${{ inputs.issue_number || 0 }}; @@ -88,7 +88,7 @@ jobs: - name: Run AI assessment id: ai-assessment - uses: github/ai-assessment-comment-labeler@v1.0.1 + uses: github/ai-assessment-comment-labeler@e3bedc38cfffa9179fe4cee8f7ecc93bffb3fee7 # v1.0.1 with: token: ${{ secrets.GITHUB_TOKEN }} issue_number: ${{ steps.issue.outputs.number }} @@ -105,7 +105,7 @@ jobs: - name: Post-process triage results if: steps.ai-assessment.outputs.ai_assessments != '' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 env: ASSESSMENT_OUTPUT: ${{ steps.ai-assessment.outputs.ai_assessments }} SUPPRESS_LABELS: ${{ env.SUPPRESS_LABELS }} @@ -219,7 +219,7 @@ jobs: - name: Generate triage summary if: always() && steps.ai-assessment.outputs.ai_assessments != '' - uses: actions/github-script@v7 + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 env: ASSESSMENT_OUTPUT: ${{ steps.ai-assessment.outputs.ai_assessments }} LABEL_DECISIONS: ${{ env.LABEL_DECISIONS }} @@ -272,7 +272,7 @@ jobs: - name: Upload triage report if: always() && steps.ai-assessment.outputs.ai_assessments != '' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: triage-report-issue-${{ steps.issue.outputs.number }} path: triage-reports/ diff --git a/.github/workflows/changelog-existence.yml b/.github/workflows/changelog-existence.yml index 72d9ffbd..affdd1d5 100644 --- a/.github/workflows/changelog-existence.yml +++ b/.github/workflows/changelog-existence.yml @@ -31,13 +31,13 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} steps: - name: ⤵️ Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 # Required to access full commit history - name: ✔️ Check for changelog changes id: changelog_check - uses: actions/github-script@v6 + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 with: script: | const { execSync } = require('child_process'); @@ -51,7 +51,7 @@ jobs: - name: 🚧 Setup Node if: steps.changelog_check.outputs.exists == 'true' - uses: actions/setup-node@v6 + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 with: node-version: '20' diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 12603c36..d459c36b 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -18,7 +18,7 @@ jobs: contents: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 # Required to access full commit history for validation diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 3b64ca39..1f2d02f7 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: ref: main - name: Install Python - uses: actions/setup-python@v5 + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 with: python-version: "3.10" - name: Install Requirements diff --git a/.github/workflows/fab-build.yml b/.github/workflows/fab-build.yml index c90b2dbf..0095ceb3 100644 --- a/.github/workflows/fab-build.yml +++ b/.github/workflows/fab-build.yml @@ -21,10 +21,10 @@ jobs: name: Lint Code runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3.7.0 - name: Set up Python 3.12 - uses: actions/setup-python@v4 + uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1 with: python-version: "3.12" # Use any stable Python version for linting @@ -34,7 +34,7 @@ jobs: pip install tox - name: Cache Tox environments - uses: actions/cache@v3 + uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 with: path: .tox key: ${{ runner.os }}-tox-lint-${{ hashFiles('**/tox.toml') }} @@ -50,10 +50,10 @@ jobs: name: Type Check Code runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3.7.0 - name: Set up Python 3.12 - uses: actions/setup-python@v4 + uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1 with: python-version: "3.12" # Use any stable Python version for type checking @@ -63,7 +63,7 @@ jobs: pip install tox - name: Cache Tox environments - uses: actions/cache@v3 + uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 with: path: .tox key: ${{ runner.os }}-tox-type-${{ hashFiles('**/tox.toml') }} @@ -89,10 +89,10 @@ jobs: - python-version: "3.13" tox-env: "py313" steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3.7.0 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1 with: python-version: ${{ matrix.python-version }} @@ -102,7 +102,7 @@ jobs: pip install tox - name: Cache Tox environments - uses: actions/cache@v3 + uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 with: path: .tox key: ${{ runner.os }}-tox-${{ matrix.tox-env }}-${{ hashFiles('**/tox.toml') }} @@ -115,7 +115,7 @@ jobs: # Upload the coverage report as an artifact - name: Upload coverage artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: coverage-html-report-${{ matrix.python-version }} path: coverage_html @@ -127,10 +127,10 @@ jobs: needs: - test # Ensure all test jobs complete successfully steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3.7.0 - name: Set up Python 3.12 - uses: actions/setup-python@v4 + uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1 with: python-version: "3.12" diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml index 2b68f112..3c8b05e6 100644 --- a/.github/workflows/semantic-pull-request.yml +++ b/.github/workflows/semantic-pull-request.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Run Semantic PR Validation id: validation - uses: actions/github-script@v6 + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 with: script: | const prTitle = context.payload.pull_request.title; @@ -33,7 +33,7 @@ jobs: } - name: Handle Invalid Title if: failure() - uses: actions/github-script@v6 + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 with: script: | const commentMarker = ''; @@ -95,7 +95,7 @@ jobs: }); - name: Handle Valid Title if: success() - uses: actions/github-script@v6 + uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 with: script: | const { data: labels } = await github.rest.issues.listLabelsOnIssue({ From 410df478ab39f5260fa600c8f280b464c9033331 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 12:25:28 +0300 Subject: [PATCH 08/15] chore(deps): bump the github-actions group with 6 updates (#278) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ai-issue-triage.yml | 10 ++++----- .github/workflows/changelog-existence.yml | 6 +++--- .github/workflows/create-release.yml | 2 +- .github/workflows/documentation.yml | 4 ++-- .github/workflows/fab-build.yml | 24 ++++++++++----------- .github/workflows/semantic-pull-request.yml | 6 +++--- 6 files changed, 26 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ai-issue-triage.yml b/.github/workflows/ai-issue-triage.yml index f58389b2..4cd0b191 100644 --- a/.github/workflows/ai-issue-triage.yml +++ b/.github/workflows/ai-issue-triage.yml @@ -30,11 +30,11 @@ jobs: steps: - name: Checkout - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Resolve issue details id: issue - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const issueNumber = context.payload.issue?.number || ${{ inputs.issue_number || 0 }}; @@ -105,7 +105,7 @@ jobs: - name: Post-process triage results if: steps.ai-assessment.outputs.ai_assessments != '' - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: ASSESSMENT_OUTPUT: ${{ steps.ai-assessment.outputs.ai_assessments }} SUPPRESS_LABELS: ${{ env.SUPPRESS_LABELS }} @@ -219,7 +219,7 @@ jobs: - name: Generate triage summary if: always() && steps.ai-assessment.outputs.ai_assessments != '' - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: ASSESSMENT_OUTPUT: ${{ steps.ai-assessment.outputs.ai_assessments }} LABEL_DECISIONS: ${{ env.LABEL_DECISIONS }} @@ -272,7 +272,7 @@ jobs: - name: Upload triage report if: always() && steps.ai-assessment.outputs.ai_assessments != '' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: triage-report-issue-${{ steps.issue.outputs.number }} path: triage-reports/ diff --git a/.github/workflows/changelog-existence.yml b/.github/workflows/changelog-existence.yml index affdd1d5..643f610b 100644 --- a/.github/workflows/changelog-existence.yml +++ b/.github/workflows/changelog-existence.yml @@ -31,13 +31,13 @@ jobs: HEAD_SHA: ${{ github.event.pull_request.head.sha }} steps: - name: ⤵️ Checkout - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # Required to access full commit history - name: ✔️ Check for changelog changes id: changelog_check - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { execSync } = require('child_process'); @@ -51,7 +51,7 @@ jobs: - name: 🚧 Setup Node if: steps.changelog_check.outputs.exists == 'true' - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0 + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: '20' diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index d459c36b..a30a1e3c 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -18,7 +18,7 @@ jobs: contents: write steps: - name: Checkout code - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 # Required to access full commit history for validation diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 1f2d02f7..26dc18e2 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: main - name: Install Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" - name: Install Requirements diff --git a/.github/workflows/fab-build.yml b/.github/workflows/fab-build.yml index 0095ceb3..aff68884 100644 --- a/.github/workflows/fab-build.yml +++ b/.github/workflows/fab-build.yml @@ -21,10 +21,10 @@ jobs: name: Lint Code runs-on: ubuntu-latest steps: - - uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3.7.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python 3.12 - uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" # Use any stable Python version for linting @@ -34,7 +34,7 @@ jobs: pip install tox - name: Cache Tox environments - uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .tox key: ${{ runner.os }}-tox-lint-${{ hashFiles('**/tox.toml') }} @@ -50,10 +50,10 @@ jobs: name: Type Check Code runs-on: ubuntu-latest steps: - - uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3.7.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python 3.12 - uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" # Use any stable Python version for type checking @@ -63,7 +63,7 @@ jobs: pip install tox - name: Cache Tox environments - uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .tox key: ${{ runner.os }}-tox-type-${{ hashFiles('**/tox.toml') }} @@ -89,10 +89,10 @@ jobs: - python-version: "3.13" tox-env: "py313" steps: - - uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3.7.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} @@ -102,7 +102,7 @@ jobs: pip install tox - name: Cache Tox environments - uses: actions/cache@6f8efc29b200d32929f49075959781ed54ec270c # v3.5.0 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: .tox key: ${{ runner.os }}-tox-${{ matrix.tox-env }}-${{ hashFiles('**/tox.toml') }} @@ -115,7 +115,7 @@ jobs: # Upload the coverage report as an artifact - name: Upload coverage artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: coverage-html-report-${{ matrix.python-version }} path: coverage_html @@ -127,10 +127,10 @@ jobs: needs: - test # Ensure all test jobs complete successfully steps: - - uses: actions/checkout@a37ce9120846195fa4ece8f58b268e6043cb2f26 # v3.7.0 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python 3.12 - uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.12" diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml index 3c8b05e6..edec8422 100644 --- a/.github/workflows/semantic-pull-request.yml +++ b/.github/workflows/semantic-pull-request.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Run Semantic PR Validation id: validation - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const prTitle = context.payload.pull_request.title; @@ -33,7 +33,7 @@ jobs: } - name: Handle Invalid Title if: failure() - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const commentMarker = ''; @@ -95,7 +95,7 @@ jobs: }); - name: Handle Valid Title if: success() - uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const { data: labels } = await github.rest.issues.listLabelsOnIssue({ From b546e44a524bdc3758a19a75a6ad4afad0eefdc8 Mon Sep 17 00:00:00 2001 From: Alon Yeshurun Date: Tue, 25 Aug 2026 17:03:58 +0300 Subject: [PATCH 09/15] chore: make dev container setup work without npm registry access The Node.js dev container feature failed to install on networks that block registry.npmjs.org, breaking container creation before setup could start. Node was only present to run `npm install -g changie`. changie ships as a standalone Go binary, so install it directly from its upstream GitHub release with checksum verification and drop the Node feature entirely. This removes a full JavaScript toolchain from an otherwise Python-only container. Also fix two latent issues in the setup script: - apt-get and the binary install now use sudo when not running as root. postCreateCommand runs as the remote user, where the previous bare apt-get exited 100 and aborted the script under `set -e`. - Support an optional git-ignored .devcontainer/local.env for environments that need an internal package mirror. pypi.org can be reachable while files.pythonhosted.org is blocked, which makes pip resolve and then fail on download. Verified end to end in the base image on amd64 and arm64, as both root and the non-root remote user. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c37de44-e0a0-4090-8f7e-299889108578 --- .devcontainer/devcontainer.json | 3 +- .devcontainer/local.env.example | 21 ++++++++ .gitignore | 1 + scripts/install_dev_container_dependencies.sh | 51 ++++++++++++++++++- 4 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 .devcontainer/local.env.example diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 28dfc7b9..460512a0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -5,8 +5,7 @@ // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile "image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye", "features": { - "ghcr.io/devcontainers/features/azure-cli:1": {}, - "ghcr.io/devcontainers/features/node:2": {} + "ghcr.io/devcontainers/features/azure-cli:1": {} }, // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, diff --git a/.devcontainer/local.env.example b/.devcontainer/local.env.example new file mode 100644 index 00000000..775371e9 --- /dev/null +++ b/.devcontainer/local.env.example @@ -0,0 +1,21 @@ +# Optional local overrides for the dev container setup script. +# +# Copy this file to `.devcontainer/local.env` and edit it. That path is +# git-ignored, so private mirror URLs never end up in the repository. +# +# cp .devcontainer/local.env.example .devcontainer/local.env +# +# It is sourced by scripts/install_dev_container_dependencies.sh before any +# packages are installed. Use plain `KEY=value` lines; the file is sourced by +# bash, so avoid spaces around `=` and quote values containing special characters. +# +# This is useful on corporate networks that block public package registries. +# Note that pypi.org may be reachable while files.pythonhosted.org is blocked, +# which makes pip resolve dependencies and then fail during download. + +# Route pip through an internal mirror instead of PyPI. +# PIP_INDEX_URL=https://your-mirror.example.com/pypi/simple/ + +# Route apt through an internal proxy. +# http_proxy=http://your-proxy.example.com:8080 +# https_proxy=http://your-proxy.example.com:8080 diff --git a/.gitignore b/.gitignore index 1fc5ba85..6e7cf5d2 100644 --- a/.gitignore +++ b/.gitignore @@ -210,3 +210,4 @@ marimo/_lsp/ __marimo__/ devcontainer.local.json +.devcontainer/local.env diff --git a/scripts/install_dev_container_dependencies.sh b/scripts/install_dev_container_dependencies.sh index 4a2d8095..7f94d6c4 100644 --- a/scripts/install_dev_container_dependencies.sh +++ b/scripts/install_dev_container_dependencies.sh @@ -1,7 +1,30 @@ #!/usr/bin/env bash set -e -apt-get update && apt-get install -y \ +# postCreateCommand runs as the container's remote user, which may be non-root. +# Route privileged steps through sudo, leaving the prefix empty when already +# root so the script behaves identically under either user. +if [ "$(id -u)" -eq 0 ]; then + sudo_cmd="" +else + sudo_cmd="sudo" +fi + +# Optional local overrides for restricted networks, e.g. pointing PIP_INDEX_URL at +# an internal mirror. The file is git-ignored so mirror URLs stay out of the repo. +# See .devcontainer/local.env.example. +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +local_env="${script_dir}/../.devcontainer/local.env" +if [ -f "$local_env" ]; then + echo "Applying local overrides from ${local_env}" + set -a + # shellcheck disable=SC1090 # Optional file resolved at runtime. + . "$local_env" + set +a +fi + +# shellcheck disable=SC2086 # sudo_cmd is intentionally unquoted so it expands to nothing as root. +$sudo_cmd apt-get update && $sudo_cmd apt-get install -y \ cmake \ libcairo2-dev \ pkg-config \ @@ -9,4 +32,28 @@ apt-get update && apt-get install -y \ pip3 install -r requirements-dev.txt -r requirements-docs.txt -npm install -g changie \ No newline at end of file +# changie ships as a standalone Go binary, so it is installed straight from the +# upstream GitHub release. This avoids pulling a full Node.js toolchain into the +# dev container solely to run `npm install -g changie`. +CHANGIE_VERSION="${CHANGIE_VERSION:-1.26.0}" + +case "$(uname -m)" in + x86_64) changie_arch="amd64" ;; + aarch64 | arm64) changie_arch="arm64" ;; + *) echo "Unsupported architecture for changie: $(uname -m)" >&2; exit 1 ;; +esac + +changie_archive="changie_${CHANGIE_VERSION}_linux_${changie_arch}.tar.gz" +changie_base_url="https://github.com/miniscruff/changie/releases/download/v${CHANGIE_VERSION}" +changie_tmp="$(mktemp -d)" +trap 'rm -rf "$changie_tmp"' EXIT + +curl -fsSL -o "${changie_tmp}/${changie_archive}" "${changie_base_url}/${changie_archive}" +curl -fsSL -o "${changie_tmp}/checksums.txt" "${changie_base_url}/checksums.txt" + +# Exact filename match on field 2 keeps the checksum line unambiguous. +(cd "$changie_tmp" && awk -v archive="$changie_archive" '$2 == archive' checksums.txt | sha256sum -c -) + +tar -xzf "${changie_tmp}/${changie_archive}" -C "$changie_tmp" changie +# /usr/local/bin is on PATH for the remote user; ~/.local/bin is not. +$sudo_cmd install -m 0755 "${changie_tmp}/changie" /usr/local/bin/changie From c345bbd3bb30aa2a4e69c436dd12ee635bb88cd7 Mon Sep 17 00:00:00 2001 From: Alon Yeshurun Date: Wed, 26 Aug 2026 11:24:56 +0300 Subject: [PATCH 10/15] Harden dev container setup script after review - Split apt-get update/install into separate statements. As the left operand of &&, a failing update did not trip set -e, so package installation was skipped silently and cmake was never installed. - Remove the base image's stale Yarn apt source. Its bundled RSA keyring predates Yarn's switch to an EdDSA signing key, so apt-get update fails verification and exits 100 on any network. - Parse .devcontainer/local.env as KEY=value data against an allowlist instead of sourcing it, so the file cannot run commands or clobber script variables. - Forward proxy variables explicitly through sudo, which resets the environment by default and previously discarded them. - Require exactly one matching checksum entry before verification. - Harden curl: https-only redirects, bounded retries and timeouts. - Resolve requirements files from the repo root rather than the caller's cwd. - Preflight sudo availability and non-interactive use. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c37de44-e0a0-4090-8f7e-299889108578 --- .../unreleased/fixed-20260826-112432.yaml | 6 + .devcontainer/local.env.example | 28 ++++- scripts/install_dev_container_dependencies.sh | 104 ++++++++++++++---- 3 files changed, 113 insertions(+), 25 deletions(-) create mode 100644 .changes/unreleased/fixed-20260826-112432.yaml mode change 100644 => 100755 scripts/install_dev_container_dependencies.sh diff --git a/.changes/unreleased/fixed-20260826-112432.yaml b/.changes/unreleased/fixed-20260826-112432.yaml new file mode 100644 index 00000000..b3c5f30e --- /dev/null +++ b/.changes/unreleased/fixed-20260826-112432.yaml @@ -0,0 +1,6 @@ +kind: fixed +body: Fix dev container creation by removing the unused Node.js toolchain, installing changie from its upstream release, dropping the stale Yarn apt source that broke apt-get update, and reporting package installation failures instead of skipping them silently +time: 2026-08-26T11:24:32+03:00 +custom: + Author: ayeshurun + AuthorLink: https://github.com/ayeshurun diff --git a/.devcontainer/local.env.example b/.devcontainer/local.env.example index 775371e9..e848c80b 100644 --- a/.devcontainer/local.env.example +++ b/.devcontainer/local.env.example @@ -1,13 +1,19 @@ # Optional local overrides for the dev container setup script. # # Copy this file to `.devcontainer/local.env` and edit it. That path is -# git-ignored, so private mirror URLs never end up in the repository. +# git-ignored, which helps prevent private mirror URLs from being committed by +# accident. It is not a secrets store: the file is plaintext, readable by +# anything in the workspace, and `git add -f` still overrides the ignore rule. +# Prefer short-lived or credential-free mirror URLs. # # cp .devcontainer/local.env.example .devcontainer/local.env # -# It is sourced by scripts/install_dev_container_dependencies.sh before any -# packages are installed. Use plain `KEY=value` lines; the file is sourced by -# bash, so avoid spaces around `=` and quote values containing special characters. +# scripts/install_dev_container_dependencies.sh reads this file before any +# packages are installed. Lines are parsed as plain `KEY=value` data and are +# never executed as shell, so this file cannot run commands. Only the keys +# listed below are applied; anything else is reported and ignored. Surrounding +# single or double quotes are stripped, and leading/trailing spaces around the +# key and value are ignored. # # This is useful on corporate networks that block public package registries. # Note that pypi.org may be reachable while files.pythonhosted.org is blocked, @@ -15,7 +21,19 @@ # Route pip through an internal mirror instead of PyPI. # PIP_INDEX_URL=https://your-mirror.example.com/pypi/simple/ +# PIP_EXTRA_INDEX_URL= +# PIP_TRUSTED_HOST= -# Route apt through an internal proxy. +# Raise pip's per-request timeout and retry count. Useful when an internal +# mirror is reachable but intermittently slow; pip defaults to 15s and 5 tries. +# PIP_TIMEOUT=60 +# PIP_RETRIES=10 + +# Route apt through an internal proxy. These three are forwarded explicitly +# through sudo, which otherwise discards them. # http_proxy=http://your-proxy.example.com:8080 # https_proxy=http://your-proxy.example.com:8080 +# no_proxy=localhost,127.0.0.1,.internal.example.com + +# Install a different changie release than the version pinned in the script. +# CHANGIE_VERSION=1.26.0 diff --git a/scripts/install_dev_container_dependencies.sh b/scripts/install_dev_container_dependencies.sh old mode 100644 new mode 100755 index 7f94d6c4..f94f2773 --- a/scripts/install_dev_container_dependencies.sh +++ b/scripts/install_dev_container_dependencies.sh @@ -1,36 +1,87 @@ #!/usr/bin/env bash -set -e +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/.." && pwd)" # postCreateCommand runs as the container's remote user, which may be non-root. -# Route privileged steps through sudo, leaving the prefix empty when already -# root so the script behaves identically under either user. +# Route privileged steps through sudo, leaving the prefix empty when already root +# so the script behaves identically under either user. sudo resets the +# environment by default, so proxy variables are preserved explicitly for apt. if [ "$(id -u)" -eq 0 ]; then - sudo_cmd="" + sudo_cmd=() else - sudo_cmd="sudo" + if ! command -v sudo >/dev/null 2>&1; then + echo "Not running as root and sudo is unavailable; cannot install packages." >&2 + exit 1 + fi + if ! sudo -n true >/dev/null 2>&1; then + echo "sudo requires a password; expected passwordless sudo in the dev container." >&2 + exit 1 + fi + sudo_cmd=(sudo -n --preserve-env=http_proxy,https_proxy,no_proxy) fi # Optional local overrides for restricted networks, e.g. pointing PIP_INDEX_URL at # an internal mirror. The file is git-ignored so mirror URLs stay out of the repo. -# See .devcontainer/local.env.example. -script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -local_env="${script_dir}/../.devcontainer/local.env" +# Lines are parsed as plain KEY=value data rather than sourced, so the file cannot +# run commands or clobber this script's own variables. See local.env.example. +local_env="${repo_root}/.devcontainer/local.env" if [ -f "$local_env" ]; then echo "Applying local overrides from ${local_env}" - set -a - # shellcheck disable=SC1090 # Optional file resolved at runtime. - . "$local_env" - set +a + while IFS= read -r raw_line || [ -n "$raw_line" ]; do + line="${raw_line%$'\r'}" + case "$line" in ''|'#'*) continue ;; esac + case "$line" in *=*) ;; *) continue ;; esac + + key="${line%%=*}" + value="${line#*=}" + key="${key#"${key%%[![:space:]]*}"}" + key="${key%"${key##*[![:space:]]}"}" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + case "$value" in + \"*\") value="${value#\"}"; value="${value%\"}" ;; + \'*\') value="${value#\'}"; value="${value%\'}" ;; + esac + + case "$key" in + PIP_INDEX_URL|PIP_EXTRA_INDEX_URL|PIP_TRUSTED_HOST|\ +PIP_RETRIES|PIP_TIMEOUT|CHANGIE_VERSION|\ +http_proxy|https_proxy|no_proxy|HTTP_PROXY|HTTPS_PROXY|NO_PROXY) + export "${key}=${value}" + echo " applied ${key}" + ;; + *) + echo " ignoring unsupported key: ${key}" >&2 + ;; + esac + done < "$local_env" +fi + +# The base image ships a Yarn apt source whose bundled keyring predates Yarn's +# switch to an EdDSA signing key, so `apt-get update` fails verification and +# exits 100 on an otherwise healthy network. Nothing here needs Yarn, so the +# stale source is removed rather than worked around. +yarn_list="/etc/apt/sources.list.d/yarn.list" +if [ -f "$yarn_list" ]; then + echo "Removing stale Yarn apt source (${yarn_list})" + "${sudo_cmd[@]}" rm -f "$yarn_list" fi -# shellcheck disable=SC2086 # sudo_cmd is intentionally unquoted so it expands to nothing as root. -$sudo_cmd apt-get update && $sudo_cmd apt-get install -y \ +# Kept as separate statements: under `set -e` a failing left-hand side of `&&` +# does not abort the script, which previously let package installation be skipped +# silently. +"${sudo_cmd[@]}" apt-get update +"${sudo_cmd[@]}" apt-get install -y \ cmake \ libcairo2-dev \ pkg-config \ python3-dev -pip3 install -r requirements-dev.txt -r requirements-docs.txt +# Not routed through sudo: pip falls back to a --user install when site-packages +# is not writable, and sudo would discard PIP_INDEX_URL set above. +pip3 install -r "${repo_root}/requirements-dev.txt" -r "${repo_root}/requirements-docs.txt" # changie ships as a standalone Go binary, so it is installed straight from the # upstream GitHub release. This avoids pulling a full Node.js toolchain into the @@ -48,12 +99,25 @@ changie_base_url="https://github.com/miniscruff/changie/releases/download/v${CHA changie_tmp="$(mktemp -d)" trap 'rm -rf "$changie_tmp"' EXIT -curl -fsSL -o "${changie_tmp}/${changie_archive}" "${changie_base_url}/${changie_archive}" -curl -fsSL -o "${changie_tmp}/checksums.txt" "${changie_base_url}/checksums.txt" +curl_opts=( + --fail --silent --show-error --location + --proto '=https' --proto-redir '=https' + --retry 3 --retry-connrefused + --connect-timeout 15 --max-time 300 +) +curl "${curl_opts[@]}" -o "${changie_tmp}/${changie_archive}" "${changie_base_url}/${changie_archive}" +curl "${curl_opts[@]}" -o "${changie_tmp}/checksums.txt" "${changie_base_url}/checksums.txt" -# Exact filename match on field 2 keeps the checksum line unambiguous. -(cd "$changie_tmp" && awk -v archive="$changie_archive" '$2 == archive' checksums.txt | sha256sum -c -) +# Require exactly one checksum line matching the archive name on field 2, so a +# truncated or unexpected checksums.txt cannot skip verification. +checksum_line="$(awk -v archive="$changie_archive" '$2 == archive' "${changie_tmp}/checksums.txt")" +match_count="$(printf '%s' "$checksum_line" | grep -c . || true)" +if [ "$match_count" -ne 1 ]; then + echo "Expected 1 checksum entry for ${changie_archive}, found ${match_count}." >&2 + exit 1 +fi +(cd "$changie_tmp" && printf '%s\n' "$checksum_line" | sha256sum -c -) tar -xzf "${changie_tmp}/${changie_archive}" -C "$changie_tmp" changie # /usr/local/bin is on PATH for the remote user; ~/.local/bin is not. -$sudo_cmd install -m 0755 "${changie_tmp}/changie" /usr/local/bin/changie +"${sudo_cmd[@]}" install -m 0755 "${changie_tmp}/changie" /usr/local/bin/changie From a5cd2f00dfa33c3fb440d8ef4e2a257a50b4ee30 Mon Sep 17 00:00:00 2001 From: Alon Yeshurun Date: Wed, 26 Aug 2026 11:56:41 +0300 Subject: [PATCH 11/15] fix(devcontainer): harden sudo prefix and document apt proxy casing Silence a ShellCheck SC2054 false positive by quoting the sudo --preserve-env argument, whose commas belong to sudo's option list rather than to the bash array. Use `env` as a no-op prefix on the root path instead of an empty array. Expanding an empty array under `set -u` is an error on bash < 4.4. Document that apt honours only lowercase proxy variables, so setting HTTP_PROXY alone leaves apt without a proxy while pip and curl work. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c37de44-e0a0-4090-8f7e-299889108578 --- .devcontainer/local.env.example | 5 +++++ scripts/install_dev_container_dependencies.sh | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.devcontainer/local.env.example b/.devcontainer/local.env.example index e848c80b..d8d6fa98 100644 --- a/.devcontainer/local.env.example +++ b/.devcontainer/local.env.example @@ -31,6 +31,11 @@ # Route apt through an internal proxy. These three are forwarded explicitly # through sudo, which otherwise discards them. +# +# Use the lowercase names. apt honours only lowercase proxy variables, so +# setting HTTP_PROXY alone leaves pip and curl working while apt silently +# gets no proxy at all. The uppercase names are accepted here for pip and +# curl, but set the lowercase ones too if apt needs the proxy. # http_proxy=http://your-proxy.example.com:8080 # https_proxy=http://your-proxy.example.com:8080 # no_proxy=localhost,127.0.0.1,.internal.example.com diff --git a/scripts/install_dev_container_dependencies.sh b/scripts/install_dev_container_dependencies.sh index f94f2773..a942081d 100755 --- a/scripts/install_dev_container_dependencies.sh +++ b/scripts/install_dev_container_dependencies.sh @@ -9,7 +9,9 @@ repo_root="$(cd "${script_dir}/.." && pwd)" # so the script behaves identically under either user. sudo resets the # environment by default, so proxy variables are preserved explicitly for apt. if [ "$(id -u)" -eq 0 ]; then - sudo_cmd=() + # `env` is a no-op prefix. An empty array would be equivalent on bash >= 4.4, + # but expanding one under `set -u` is an error on older bash. + sudo_cmd=(env) else if ! command -v sudo >/dev/null 2>&1; then echo "Not running as root and sudo is unavailable; cannot install packages." >&2 @@ -19,7 +21,8 @@ else echo "sudo requires a password; expected passwordless sudo in the dev container." >&2 exit 1 fi - sudo_cmd=(sudo -n --preserve-env=http_proxy,https_proxy,no_proxy) + # Quoted because the commas belong to sudo's --preserve-env list, not to the array. + sudo_cmd=(sudo -n "--preserve-env=http_proxy,https_proxy,no_proxy") fi # Optional local overrides for restricted networks, e.g. pointing PIP_INDEX_URL at From d8e07e740157c446a8e419218cbaa26e49f34e6e Mon Sep 17 00:00:00 2001 From: Alon Yeshurun Date: Wed, 26 Aug 2026 18:31:02 +0300 Subject: [PATCH 12/15] fix(devcontainer): install changie from archive contents, not archive entry A tar member named `changie` could be a symlink or hard link. The previous `tar -xzf` materialised that entry, and the following `install` under sudo followed it, copying an arbitrary root-readable file into world-readable /usr/local/bin. Verified in mcr.microsoft.com/devcontainers/python:1-3.12-bullseye: a `changie -> /etc/shadow` member produced a 0755 root-owned copy of /etc/shadow readable by the unprivileged remote user. Extract the member's contents with `tar -O` instead. Link entries carry no content, so they yield zero bytes and the added non-empty check rejects them; the shell redirect always creates a regular file inside the 0700 mktemp directory, removing link semantics from the privileged step entirely. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c37de44-e0a0-4090-8f7e-299889108578 --- scripts/install_dev_container_dependencies.sh | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/scripts/install_dev_container_dependencies.sh b/scripts/install_dev_container_dependencies.sh index a942081d..c094af43 100755 --- a/scripts/install_dev_container_dependencies.sh +++ b/scripts/install_dev_container_dependencies.sh @@ -121,6 +121,17 @@ if [ "$match_count" -ne 1 ]; then fi (cd "$changie_tmp" && printf '%s\n' "$checksum_line" | sha256sum -c -) -tar -xzf "${changie_tmp}/${changie_archive}" -C "$changie_tmp" changie +# Extract the member's contents rather than materialising the archive entry. A +# malformed or substituted archive could ship `changie` as a symlink or hard +# link, and `install` running under sudo would follow it and copy an arbitrary +# root-readable file into world-readable /usr/local/bin. `tar -O` emits nothing +# for link entries, and the redirect always creates a regular file inside the +# 0700 temp directory, so this path fails closed. +tar -xzOf "${changie_tmp}/${changie_archive}" changie > "${changie_tmp}/changie.bin" +if [ ! -s "${changie_tmp}/changie.bin" ]; then + echo "Extracted changie is empty; refusing to install." >&2 + exit 1 +fi + # /usr/local/bin is on PATH for the remote user; ~/.local/bin is not. -"${sudo_cmd[@]}" install -m 0755 "${changie_tmp}/changie" /usr/local/bin/changie +"${sudo_cmd[@]}" install -m 0755 "${changie_tmp}/changie.bin" /usr/local/bin/changie From 5e128abcaa6a7f2b1f4fd30805fd235358f009dd Mon Sep 17 00:00:00 2001 From: Alon Yeshurun Date: Wed, 26 Aug 2026 18:55:39 +0300 Subject: [PATCH 13/15] docs(devcontainer): scope the tar extraction guard comment accurately The comment claimed the install path "fails closed". That is only true for link entries. Verified in the target image that a directory member named `changie` streams its child's contents through `tar -O` (21 bytes, exit 0), passing the non-empty check; duplicate members concatenate similarly. Neither is exploitable: the bytes originate inside the checksum-gated archive, never from a host path, so the guard still prevents the privileged install from following a link into the filesystem. Reword to state that bound precisely rather than overclaiming. Comment-only change; bash -n and shellcheck --severity=style pass. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0c37de44-e0a0-4090-8f7e-299889108578 --- scripts/install_dev_container_dependencies.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/scripts/install_dev_container_dependencies.sh b/scripts/install_dev_container_dependencies.sh index c094af43..e5e966a2 100755 --- a/scripts/install_dev_container_dependencies.sh +++ b/scripts/install_dev_container_dependencies.sh @@ -124,9 +124,14 @@ fi # Extract the member's contents rather than materialising the archive entry. A # malformed or substituted archive could ship `changie` as a symlink or hard # link, and `install` running under sudo would follow it and copy an arbitrary -# root-readable file into world-readable /usr/local/bin. `tar -O` emits nothing -# for link entries, and the redirect always creates a regular file inside the -# 0700 temp directory, so this path fails closed. +# root-readable file into world-readable /usr/local/bin. `tar -O` emits no +# content for link entries, so the empty check below rejects them, and the +# redirect always writes a regular file inside the 0700 temp directory. +# +# This bounds the outcome to bytes carried by the archive itself. A substituted +# archive can still stream a directory child or concatenated duplicate members +# through this path, which is no worse than it shipping a malicious regular +# member; what it cannot do is make the privileged install read a host path. tar -xzOf "${changie_tmp}/${changie_archive}" changie > "${changie_tmp}/changie.bin" if [ ! -s "${changie_tmp}/changie.bin" ]; then echo "Extracted changie is empty; refusing to install." >&2 From 98e63de10d046eed96bf98872c1677492ea7542d Mon Sep 17 00:00:00 2001 From: Alon Yeshurun <98805507+ayeshurun@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:18:45 +0300 Subject: [PATCH 14/15] Delete .devcontainer/local.env.example --- .devcontainer/local.env.example | 44 --------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 .devcontainer/local.env.example diff --git a/.devcontainer/local.env.example b/.devcontainer/local.env.example deleted file mode 100644 index d8d6fa98..00000000 --- a/.devcontainer/local.env.example +++ /dev/null @@ -1,44 +0,0 @@ -# Optional local overrides for the dev container setup script. -# -# Copy this file to `.devcontainer/local.env` and edit it. That path is -# git-ignored, which helps prevent private mirror URLs from being committed by -# accident. It is not a secrets store: the file is plaintext, readable by -# anything in the workspace, and `git add -f` still overrides the ignore rule. -# Prefer short-lived or credential-free mirror URLs. -# -# cp .devcontainer/local.env.example .devcontainer/local.env -# -# scripts/install_dev_container_dependencies.sh reads this file before any -# packages are installed. Lines are parsed as plain `KEY=value` data and are -# never executed as shell, so this file cannot run commands. Only the keys -# listed below are applied; anything else is reported and ignored. Surrounding -# single or double quotes are stripped, and leading/trailing spaces around the -# key and value are ignored. -# -# This is useful on corporate networks that block public package registries. -# Note that pypi.org may be reachable while files.pythonhosted.org is blocked, -# which makes pip resolve dependencies and then fail during download. - -# Route pip through an internal mirror instead of PyPI. -# PIP_INDEX_URL=https://your-mirror.example.com/pypi/simple/ -# PIP_EXTRA_INDEX_URL= -# PIP_TRUSTED_HOST= - -# Raise pip's per-request timeout and retry count. Useful when an internal -# mirror is reachable but intermittently slow; pip defaults to 15s and 5 tries. -# PIP_TIMEOUT=60 -# PIP_RETRIES=10 - -# Route apt through an internal proxy. These three are forwarded explicitly -# through sudo, which otherwise discards them. -# -# Use the lowercase names. apt honours only lowercase proxy variables, so -# setting HTTP_PROXY alone leaves pip and curl working while apt silently -# gets no proxy at all. The uppercase names are accepted here for pip and -# curl, but set the lowercase ones too if apt needs the proxy. -# http_proxy=http://your-proxy.example.com:8080 -# https_proxy=http://your-proxy.example.com:8080 -# no_proxy=localhost,127.0.0.1,.internal.example.com - -# Install a different changie release than the version pinned in the script. -# CHANGIE_VERSION=1.26.0 From ffaaa5b9f66de2ee8b5325ec535f112e67b7035f Mon Sep 17 00:00:00 2001 From: Alon Yeshurun Date: Sun, 30 Aug 2026 11:40:55 +0300 Subject: [PATCH 15/15] docs(devcontainer): restore local.env.example and document the override Commit 98e63de deleted .devcontainer/local.env.example, which left install_dev_container_dependencies.sh pointing at a file that no longer existed ("See local.env.example.") and removed the only description of the override mechanism. Nothing in the docs mentioned local.env, so a contributor on a network that blocks public package registries had no way to discover the escape hatch short of reading the setup script. Restore the example file and add a CONTRIBUTING section pointing at it. The example uses placeholder hosts only, so no internal URL is exposed, and .devcontainer/local.env stays git-ignored. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0a0e160d-7d85-43c6-972e-c0365d4bf9aa --- .devcontainer/local.env.example | 44 +++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 12 +++++++++ 2 files changed, 56 insertions(+) create mode 100644 .devcontainer/local.env.example diff --git a/.devcontainer/local.env.example b/.devcontainer/local.env.example new file mode 100644 index 00000000..d8d6fa98 --- /dev/null +++ b/.devcontainer/local.env.example @@ -0,0 +1,44 @@ +# Optional local overrides for the dev container setup script. +# +# Copy this file to `.devcontainer/local.env` and edit it. That path is +# git-ignored, which helps prevent private mirror URLs from being committed by +# accident. It is not a secrets store: the file is plaintext, readable by +# anything in the workspace, and `git add -f` still overrides the ignore rule. +# Prefer short-lived or credential-free mirror URLs. +# +# cp .devcontainer/local.env.example .devcontainer/local.env +# +# scripts/install_dev_container_dependencies.sh reads this file before any +# packages are installed. Lines are parsed as plain `KEY=value` data and are +# never executed as shell, so this file cannot run commands. Only the keys +# listed below are applied; anything else is reported and ignored. Surrounding +# single or double quotes are stripped, and leading/trailing spaces around the +# key and value are ignored. +# +# This is useful on corporate networks that block public package registries. +# Note that pypi.org may be reachable while files.pythonhosted.org is blocked, +# which makes pip resolve dependencies and then fail during download. + +# Route pip through an internal mirror instead of PyPI. +# PIP_INDEX_URL=https://your-mirror.example.com/pypi/simple/ +# PIP_EXTRA_INDEX_URL= +# PIP_TRUSTED_HOST= + +# Raise pip's per-request timeout and retry count. Useful when an internal +# mirror is reachable but intermittently slow; pip defaults to 15s and 5 tries. +# PIP_TIMEOUT=60 +# PIP_RETRIES=10 + +# Route apt through an internal proxy. These three are forwarded explicitly +# through sudo, which otherwise discards them. +# +# Use the lowercase names. apt honours only lowercase proxy variables, so +# setting HTTP_PROXY alone leaves pip and curl working while apt silently +# gets no proxy at all. The uppercase names are accepted here for pip and +# curl, but set the lowercase ones too if apt needs the proxy. +# http_proxy=http://your-proxy.example.com:8080 +# https_proxy=http://your-proxy.example.com:8080 +# no_proxy=localhost,127.0.0.1,.internal.example.com + +# Install a different changie release than the version pinned in the script. +# CHANGIE_VERSION=1.26.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9e1e7128..003f459f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -62,6 +62,18 @@ All PRs must be linked with a "help wanted" issue. To avoid rework after investi Please review [engineering guidelines](https://github.com/microsoft/fabric-cli/wiki) for coding guidelines and common flows to help you with your task. +### Development environment + +The repository ships a [dev container](https://containers.dev). Opening the project in it installs the build dependencies, the Python requirements, and `changie`. + +On a restricted network that blocks public package registries, point the setup script at an internal mirror instead: + +```bash +cp .devcontainer/local.env.example .devcontainer/local.env +``` + +Then uncomment and edit the entries you need, and rebuild the container. `.devcontainer/local.env` is git-ignored, so mirror URLs stay out of the repository. See the comments in the example file for the supported keys. + ### Pull request process Please use a descriptive title and provide a clear summary of your changes.