Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/artifacts-helper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ Configures Codespace to authenticate with Azure Artifact feeds
| pnpmAlias | Create alias for pnpm | boolean | true |
| targetFiles | Comma separated list of files to write to. Default is '/etc/bash.bashrc,/etc/zsh/zshrc' for root and '~/.bashrc,~/.zshrc' for non-root | string | DEFAULT |
| python | Install Python keyring helper for pip | boolean | false |
| wrapperType | Type of wrapper to use. Options are 'SHELL_FUNCTION' or 'EXECUTABLE' | string | `SHELL_FUNCTION` |

### Wrapper Types

| Wrapper Type | Description |
|-|-|
| `SHELL_FUNCTION` | Configures shell functions that wrap the commands to set the authentication token before calling the actual command. This is the default options. |
Copy link

Copilot AI Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'options' to 'option'.

Suggested change
| `SHELL_FUNCTION` | Configures shell functions that wrap the commands to set the authentication token before calling the actual command. This is the default options. |
| `SHELL_FUNCTION` | Configures shell functions that wrap the commands to set the authentication token before calling the actual command. This is the default option. |

Copilot uses AI. Check for mistakes.
| `EXECUTABLE` | Configures a separate executable that wraps the commands to set the authentication token before calling the actual command. Suited for scenarios where the commands need to be invoked from scripts that do not consider shell functions, e.g. `subprocess.Popen(['npx'], shell=False)`. |

## Customizations

Expand Down
15 changes: 14 additions & 1 deletion src/artifacts-helper/devcontainer-feature.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,24 @@
"type": "boolean",
"default": false,
"description": "Install Python keyring helper for pip"
},
"wrapperType": {
"type": "string",
"default": "SHELL_FUNCTION",
"description": "Type of wrapper script to use.",
"enum": [
"SHELL_FUNCTION",
"EXECUTABLE"
]
}
},
"containerEnv": {
"PATH": "/usr/local/share/codespace-features:${PATH}"
},
"installsAfter": [
"ghcr.io/devcontainers/features/common-utils",
"ghcr.io/devcontainers/features/python"
"ghcr.io/devcontainers/features/python",
"ghcr.io/devcontainers/features/node"
],
"customizations": {
"vscode": {
Expand Down
77 changes: 51 additions & 26 deletions src/artifacts-helper/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ set -e

PREFIXES="${NUGETURIPREFIXES:-"https://pkgs.dev.azure.com/"}"
USENET6="${DOTNET6:-"false"}"
WRAPPERTYPE="${WRAPPERTYPE:-"SHELL_FUNCTION"}"
ALIAS_DOTNET="${DOTNETALIAS:-"true"}"
ALIAS_NUGET="${NUGETALIAS:-"true"}"
ALIAS_NPM="${NPMALIAS:-"true"}"
Expand All @@ -14,30 +15,35 @@ ALIAS_PNPM="${PNPMALIAS:-"true"}"
INSTALL_PIP_HELPER="${PYTHON:-"false"}"
COMMA_SEP_TARGET_FILES="${TARGETFILES:-"DEFAULT"}"

ALIASES_ARR=()
# Destination to install wrappers for the `EXECUTABLE` wrapper type.
# This path must take precedence over /usr/local/bin and the original tools,
# which is ensured via `containerEnv.PATH` in devcontainer-feature.json.
EXECUTABLES_TARGET_DIR='/usr/local/share/codespace-features'

WRAPPED_BINS_ARR=()

if [ "${ALIAS_DOTNET}" = "true" ]; then
ALIASES_ARR+=('dotnet')
WRAPPED_BINS_ARR+=('dotnet')
fi
if [ "${ALIAS_NUGET}" = "true" ]; then
ALIASES_ARR+=('nuget')
WRAPPED_BINS_ARR+=('nuget')
fi
if [ "${ALIAS_NPM}" = "true" ]; then
ALIASES_ARR+=('npm')
WRAPPED_BINS_ARR+=('npm')
fi
if [ "${ALIAS_YARN}" = "true" ]; then
ALIASES_ARR+=('yarn')
WRAPPED_BINS_ARR+=('yarn')
fi
if [ "${ALIAS_NPX}" = "true" ]; then
ALIASES_ARR+=('npx')
WRAPPED_BINS_ARR+=('npx')
fi
if [ "${ALIAS_RUSH}" = "true" ]; then
ALIASES_ARR+=('rush')
ALIASES_ARR+=('rush-pnpm')
WRAPPED_BINS_ARR+=('rush')
WRAPPED_BINS_ARR+=('rush-pnpm')
fi
if [ "${ALIAS_PNPM}" = "true" ]; then
ALIASES_ARR+=('pnpm')
ALIASES_ARR+=('pnpx')
WRAPPED_BINS_ARR+=('pnpm')
WRAPPED_BINS_ARR+=('pnpx')
fi

# Source /etc/os-release to get OS info
Expand Down Expand Up @@ -116,27 +122,46 @@ if command -v sudo >/dev/null 2>&1; then
fi
fi

if [ "${COMMA_SEP_TARGET_FILES}" = "DEFAULT" ]; then
if [ "${INSTALL_WITH_SUDO}" = "true" ]; then
COMMA_SEP_TARGET_FILES="~/.bashrc,~/.zshrc"
else
COMMA_SEP_TARGET_FILES="/etc/bash.bashrc,/etc/zsh/zshrc"
fi
fi

IFS=',' read -r -a TARGET_FILES_ARR <<< "$COMMA_SEP_TARGET_FILES"

for ALIAS in "${ALIASES_ARR[@]}"; do
for TARGET_FILE in "${TARGET_FILES_ARR[@]}"; do
CMD="$ALIAS() { /usr/local/bin/run-$ALIAS.sh \"\$@\"; }"
if [ "$WRAPPERTYPE" = "SHELL_FUNCTION" ]; then
echo "Installing shell functions for wrapped commands."

if [ "${COMMA_SEP_TARGET_FILES}" = "DEFAULT" ]; then
if [ "${INSTALL_WITH_SUDO}" = "true" ]; then
sudo -u ${_REMOTE_USER} bash -c "echo '$CMD' >> $TARGET_FILE"
COMMA_SEP_TARGET_FILES="~/.bashrc,~/.zshrc"
else
echo $CMD >> $TARGET_FILE || true
COMMA_SEP_TARGET_FILES="/etc/bash.bashrc,/etc/zsh/zshrc"
fi
fi

IFS=',' read -r -a TARGET_FILES_ARR <<< "$COMMA_SEP_TARGET_FILES"

for ALIAS in "${WRAPPED_BINS_ARR[@]}"; do
for TARGET_FILE in "${TARGET_FILES_ARR[@]}"; do
CMD="$ALIAS() { /usr/local/bin/run-$ALIAS.sh \"\$@\"; }"

if [ "${INSTALL_WITH_SUDO}" = "true" ]; then
sudo -u ${_REMOTE_USER} bash -c "echo '$CMD' >> $TARGET_FILE"
else
echo $CMD >> $TARGET_FILE || true
fi
done
done
elif [ "$WRAPPERTYPE" = "EXECUTABLE" ]; then
echo "Installing executable scripts for wrapped commands."
sudo mkdir -p "${EXECUTABLES_TARGET_DIR}"

for ALIAS in "${WRAPPED_BINS_ARR[@]}"; do
TARGET_EXECUTABLE_PATH="${EXECUTABLES_TARGET_DIR}/$ALIAS"
TARGET_WRAPPER_PATH="/usr/local/bin/run-$ALIAS.sh"
TARGET_WRAPPER_EXE_ENV_VAR_NAME=$(sed "s|-|_|g" <<<"${ALIAS}_EXE" | tr '[:lower:]' '[:upper:]')
cp ./scripts/executable-wrapper-template.sh "$TARGET_EXECUTABLE_PATH"
sed -i "s|SUBST_TARGET_WRAPPER_PATH|$TARGET_WRAPPER_PATH|g; s|SUBST_TARGET_BIN_NAME|$ALIAS|g; s|SUBST_EXE_ENV_VAR|$TARGET_WRAPPER_EXE_ENV_VAR_NAME|g" "$TARGET_EXECUTABLE_PATH"
chmod +x "$TARGET_EXECUTABLE_PATH"
done
done
else
echo "Invalid WRAPPERTYPE specified: $WRAPPERTYPE. Must be one of SHELL_FUNCTION or EXECUTABLE."
exit 1
fi

if [ "${INSTALL_WITH_SUDO}" = "true" ]; then
sudo -u ${_REMOTE_USER} bash -c "/tmp/install-provider.sh ${USENET6}"
Expand Down
69 changes: 69 additions & 0 deletions src/artifacts-helper/scripts/executable-wrapper-template.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash

# Recursively unwrap itself from the path, eventually calling the target binary's
# `run-<target>.sh` script, which will inject a feed authentication token.

TARGET_BIN_NAME=SUBST_TARGET_BIN_NAME
TARGET_WRAPPER_PATH=SUBST_TARGET_WRAPPER_PATH

log() {
# Print to stderr to avoid interfering with scripts that capture stdout.
# Prefix error messages so the user can quickly determine that it's from our
# artifacts-helper wrapper, not an error originating from the wrapped binary.
printf >&2 "[codespace-features/artifacts-helper] %s" "$1"
}

log_line() {
log "$1
"
}

log_debug_line() {
if [ "$ARTIFACTS_HELPER_DEBUG" == "1" ]; then
log_line "$1"
fi
}

call_wrapped_bin() {
CURRENT_SCRIPT_PATH=$(realpath "${BASH_SOURCE[0]}")
if [ -z "$CURRENT_SCRIPT_PATH" ]; then
log_line "Error: Current script path could not be determined! This will result in infinite recursion. Bailing early."
exit 1
fi

# Find the next executable from the PATH order, which would be shadowed by this wrapper
if [ -z "$ARTIFACTS_HELPER_TARGET_BIN_PATHS" ]; then
ARTIFACTS_HELPER_TARGET_BIN_PATHS=$(which -a "$TARGET_BIN_NAME")
fi

ARTIFACTS_HELPER_TARGET_BIN_PATHS=$(sed '/^[[:space:]]*$/d' <<<"$ARTIFACTS_HELPER_TARGET_BIN_PATHS")
ARTIFACTS_HELPER_TARGET_BIN_PATHS=$(grep --color=never -Fve "$CURRENT_SCRIPT_PATH" <<<"$ARTIFACTS_HELPER_TARGET_BIN_PATHS")
NEXT_SHADOWED_BIN=$(head -n 1 <<<"$ARTIFACTS_HELPER_TARGET_BIN_PATHS")

log_debug_line "CURRENT_SCRIPT_PATH=$CURRENT_SCRIPT_PATH"
log_debug_line "TARGET_BIN_NAME=$TARGET_BIN_NAME"
log_debug_line "TARGET_WRAPPER_PATH=$TARGET_WRAPPER_PATH"
log_debug_line "ARTIFACTS_HELPER_TARGET_BIN_PATHS=$ARTIFACTS_HELPER_TARGET_BIN_PATHS"
log_debug_line "NEXT_SHADOWED_BIN=$NEXT_SHADOWED_BIN"

if [ -z "$NEXT_SHADOWED_BIN" ]; then
log_line "Error: The real $TARGET_BIN_NAME could not be found on PATH."
log_line "which -a $TARGET_BIN_NAME:\n%s\n" "$(which -a $TARGET_BIN_NAME)"
Copy link

Copilot AI Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log_line function uses printf which expects format arguments to be separated, but here the format specifiers %s are embedded in the first argument string. The $(which -a $TARGET_BIN_NAME) expansion won't be properly substituted. This line should either use echo or properly separate the printf format string from its arguments.

Copilot uses AI. Check for mistakes.
log_line "CC_SHADOWED_BINS:\n%s\n" "$ARTIFACTS_HELPER_TARGET_BIN_PATHS"
log_line "CURRENT_SCRIPT_PATH: %s\n" "$CURRENT_SCRIPT_PATH"
Comment on lines +52 to +53
Copy link

Copilot AI Oct 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as line 51: log_line calls printf with a single argument, so the format specifiers %s won't be replaced with the variable values in the second argument. These arguments will be ignored by printf.

Copilot uses AI. Check for mistakes.
exit 1
fi

# Recursively removing the wrapper script(s) from the shadowed bin paths will
# allow us to account for circumstances where the wrapper script appears multiple
# times on the PATH. We will eventually descend to the real target binary.
export ARTIFACTS_HELPER_TARGET_BIN_PATHS

log_debug_line "Running: SUBST_EXE_ENV_VAR=$NEXT_SHADOWED_BIN $TARGET_WRAPPER_PATH"
SUBST_EXE_ENV_VAR="$NEXT_SHADOWED_BIN" "$TARGET_WRAPPER_PATH" "$@"
return "$?"
}

call_wrapped_bin "$@"
EXIT_CODE=$?
exit $EXIT_CODE
4 changes: 3 additions & 1 deletion src/artifacts-helper/scripts/run-dotnet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ if [ -f "${HOME}/ado-auth-helper" ]; then
fi

# Find the dotnet executable so we do not run the bash alias again
DOTNET_EXE=$(which dotnet)
if [ -z "$DOTNET_EXE" ]; then
DOTNET_EXE=$(which dotnet)
fi

${DOTNET_EXE} "$@"
EXIT_CODE=$?
Expand Down
6 changes: 4 additions & 2 deletions src/artifacts-helper/scripts/run-npm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ if [ -f "${HOME}/ado-auth-helper" ]; then
export ARTIFACTS_ACCESSTOKEN=$(${HOME}/ado-auth-helper get-access-token)
fi

# Find the npm executable so we do not run the bash alias again
NPM_EXE=$(which npm)
if [ -z "$NPM_EXE" ]; then
# Find the npm executable so we do not run the bash alias again
NPM_EXE=$(which npm)
fi

${NPM_EXE} "$@"
EXIT_CODE=$?
Expand Down
6 changes: 4 additions & 2 deletions src/artifacts-helper/scripts/run-npx.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ if [ -f "${HOME}/ado-auth-helper" ]; then
export ARTIFACTS_ACCESSTOKEN=$(${HOME}/ado-auth-helper get-access-token)
fi

# Find the npm executable so we do not run the bash alias again
NPX_EXE=$(which npx)
# Find the npx executable so we do not run the bash alias again
if [ -z "$NPX_EXE" ]; then
NPX_EXE=$(which npx)
fi

${NPX_EXE} "$@"
EXIT_CODE=$?
Expand Down
6 changes: 4 additions & 2 deletions src/artifacts-helper/scripts/run-nuget.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ if [ -f "${HOME}/ado-auth-helper" ]; then
export VSS_NUGET_URI_PREFIXES=REPLACE_WITH_AZURE_DEVOPS_NUGET_FEED_URL_PREFIX
fi

# Find the dotnet executable so we do not run the bash alias again
NUGET_EXE=$(which nuget)
# Find the nuget executable so we do not run the bash alias again
if [ -z "$NUGET_EXE" ]; then
NUGET_EXE=$(which nuget)
fi

${NUGET_EXE} "$@"
EXIT_CODE=$?
Expand Down
4 changes: 3 additions & 1 deletion src/artifacts-helper/scripts/run-pnpm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ if [ -f "${HOME}/ado-auth-helper" ]; then
fi

# Find the pnpm executable so we do not run the bash alias again
PNPM_EXE=$(which pnpm)
if [ -z "$PNPM_EXE" ]; then
PNPM_EXE=$(which pnpm)
fi

${PNPM_EXE} "$@"
EXIT_CODE=$?
Expand Down
4 changes: 3 additions & 1 deletion src/artifacts-helper/scripts/run-pnpx.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ if [ -f "${HOME}/ado-auth-helper" ]; then
fi

# Find the pnpx executable so we do not run the bash alias again
PNPX_EXE=$(which pnpx)
if [ -z "$PNPX_EXE" ]; then
PNPX_EXE=$(which pnpx)
fi

${PNPX_EXE} "$@"
EXIT_CODE=$?
Expand Down
4 changes: 3 additions & 1 deletion src/artifacts-helper/scripts/run-rush-pnpm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ if [ -f "${HOME}/ado-auth-helper" ]; then
fi

# Find the rush-pnpm executable so we do not run the bash alias again
RUSH_PNPM_EXE=$(which rush-pnpm)
if [ -z "$RUSH_PNPM_EXE" ]; then
RUSH_PNPM_EXE=$(which rush-pnpm)
fi

${RUSH_PNPM_EXE} "$@"
EXIT_CODE=$?
Expand Down
4 changes: 3 additions & 1 deletion src/artifacts-helper/scripts/run-rush.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ if [ -f "${HOME}/ado-auth-helper" ]; then
fi

# Find the rush executable so we do not run the bash alias again
RUSH_EXE=$(which rush)
if [ -z "$RUSH_EXE" ]; then
RUSH_EXE=$(which rush)
fi

${RUSH_EXE} "$@"
EXIT_CODE=$?
Expand Down
4 changes: 3 additions & 1 deletion src/artifacts-helper/scripts/run-yarn.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ if [ -f "${HOME}/ado-auth-helper" ]; then
fi

# Find the yarn executable so we do not run the bash alias again
YARN_EXE=$(which yarn)
if [ -z "$YARN_EXE" ]; then
YARN_EXE=$(which yarn)
fi

${YARN_EXE} "$@"
EXIT_CODE=$?
Expand Down
Loading