Skip to content
Open
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
30 changes: 3 additions & 27 deletions src/artifacts-helper/NOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
This installs [Azure Artifacts Credential Provider](https://github.com/microsoft/artifacts-credprovider)
and optionally configures functions which shadow `dotnet`, `nuget`, `npm`, `yarn`, `rush`, and `pnpm` which dynamically sets an authentication token
for pulling artifacts from a feed before running the command.
and optionally configures shims which shadow `dotnet`, `nuget`, `npm`, `yarn`, `rush`, and `pnpm`.
These dynamically sets an authentication token for pulling artifacts from a feed before running the command.

For `npm`, `yarn`, `rush`, and `pnpm` this requires that your `~/.npmrc` file is configured to use the ${ARTIFACTS_ACCESSTOKEN}
environment variable for the `authToken`. A helper script has been added that you can use to write your `~/.npmrc`
Expand Down Expand Up @@ -40,28 +40,4 @@ to download the package.

## OS Support

This feature is tested to work on Debian/Ubuntu and Mariner CBL 2.0

## Changing where functions are configured

By default, the functions are defined in `/etc/bash.bashrc` and `/etc/zsh/zshrc` if the container user is `root`, otherwise `~/.bashrc` and `~/.zshrc`.
This default configuration ensures that the functions are always available for any interactive shells.

In some cases it can be useful to have the functions written to a non-default location. For example:
- the configuration file of a shell other than `bash` and `zsh`
- a custom file which is not a shell configuration script (so that it can be `source`d in non-interactive shells and scripts)

To do this, set the `targetFiles` option to the path script path where the functions should be written. Note that the default paths WILL NOT be used
if the `targetFiles` option is provided, so you may want to include them in the overridden value, or add `source` the custom script in those configurations:

```bash
# .devcontainer/devcontainer.json
{
// ...
"targetFiles": "/custom/path/to/auth-helper.sh"
}

# ~/.bashrc

source /custom/path/to/auth-helper.sh
```
This feature is tested to work on Debian/Ubuntu and Mariner CBL 2.0
25 changes: 25 additions & 0 deletions src/artifacts-helper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,35 @@ pip install <package_name> --index-url https://pkgs.dev.azure.com/<org_name>/_pa
When the feed URL is an Azure Artifacts feed pip will use the keyring helper to provide the credentials needed
to download the package.

## Authentication Helper Wait Behavior
Copy link
Contributor

Choose a reason for hiding this comment

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

This file cannot be edited. It is generated by the release process so this will all be lost. This content needs to go into NOTES.md and that will cause it to be added to README.md when the release is created


The shim scripts (e.g., `dotnet`, `npm`, `nuget`) now include a wait mechanism for the Azure DevOps authentication helper. When invoked, these scripts will:

1. Wait up to 3 minutes for the `ado-auth-helper` to become available (configurable via `MAX_WAIT` environment variable)
2. Display progress indicators every 20 seconds while waiting
3. Continue execution once authentication is successful
4. **Continue with the underlying command even if authentication is not available** after the timeout

This ensures that package restore operations can proceed even if there's a slight delay in the authentication helper installation, which can occur in some codespace initialization scenarios. Commands will still execute without authentication, though they may fail to access private Azure Artifacts feeds.

The scripts are designed to be sourced safely, meaning they won't terminate the calling shell if authentication fails - they will simply return an error code and allow the underlying tool to execute. This allows you to work with public packages or other package sources even when Azure Artifacts authentication is unavailable.

## OS Support

This feature is tested to work on Debian/Ubuntu and Mariner CBL 2.0

## Testing

To test this feature locally, you can use the devcontainer CLI:

```bash
# Test all scenarios
devcontainer features test -f artifacts-helper

# Test specific scenario
devcontainer features test -f artifacts-helper --scenario test_auth_wait
```

## Changing where functions are configured

By default, the functions are defined in `/etc/bash.bashrc` and `/etc/zsh/zshrc` if the container user is `root`, otherwise `~/.bashrc` and `~/.zshrc`.
Expand Down
13 changes: 11 additions & 2 deletions src/artifacts-helper/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "Azure Artifacts Credential Helper",
"id": "artifacts-helper",
"version": "2.0.3",
"version": "3.0.0",
"description": "Configures Codespace to authenticate with Azure Artifact feeds",
"options": {
"nugetURIPrefixes": {
Expand Down Expand Up @@ -49,6 +49,11 @@
"default": true,
"description": "Create alias for pnpm"
},
"shimDirectory": {
"type": "string",
"default": "/usr/local/share/codespace-shims",
"description": "Directory where the shims will be installed. This must be in $PATH, and needs to be as early as possible in priority for the scripts to override the base executables."
},
"targetFiles": {
"type": "string",
"default": "DEFAULT",
Expand All @@ -60,9 +65,13 @@
"description": "Install Python keyring helper for pip"
}
},
"containerEnv": {
"PATH": "/usr/local/share/codespace-shims:${PATH}"
Copy link
Author

@abdurriq abdurriq Dec 12, 2025

Choose a reason for hiding this comment

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

This had to be hard-coded, since using something like ${shimDirectory} doesn't work; resulting in an empty value. Apparently these cannot be resolved from option values or env variables (I also tried $SHIMDIRECTORY).

},
"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
62 changes: 30 additions & 32 deletions src/artifacts-helper/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ ALIAS_NPX="${NPXALIAS:-"true"}"
ALIAS_RUSH="${RUSHALIAS:-"true"}"
ALIAS_PNPM="${PNPMALIAS:-"true"}"
INSTALL_PIP_HELPER="${PYTHON:-"false"}"
COMMA_SEP_TARGET_FILES="${TARGETFILES:-"DEFAULT"}"
SHIM_DIRECTORY="${SHIMDIRECTORY:-"/usr/local/share/codespace-shims/"}"

ALIASES_ARR=()

Expand Down Expand Up @@ -78,31 +78,28 @@ cd "$(dirname "$0")"

cp ./scripts/install-provider.sh /tmp
chmod +rx /tmp/install-provider.sh

cp ./scripts/install-python-keyring.sh /tmp
chmod +rx /tmp/install-python-keyring.sh

sed "s|REPLACE_WITH_AZURE_DEVOPS_NUGET_FEED_URL_PREFIX|${PREFIXES}|g" ./scripts/run-dotnet.sh > /usr/local/bin/run-dotnet.sh
chmod +rx /usr/local/bin/run-dotnet.sh
sed "s|REPLACE_WITH_AZURE_DEVOPS_NUGET_FEED_URL_PREFIX|${PREFIXES}|g" ./scripts/run-nuget.sh > /usr/local/bin/run-nuget.sh
chmod +rx /usr/local/bin/run-nuget.sh
cp ./scripts/run-npm.sh /usr/local/bin/run-npm.sh
chmod +rx /usr/local/bin/run-npm.sh
cp ./scripts/run-yarn.sh /usr/local/bin/run-yarn.sh
chmod +rx /usr/local/bin/run-yarn.sh
cp ./scripts/write-npm.sh /usr/local/bin/write-npm.sh
chmod +rx /usr/local/bin/write-npm.sh
cp ./scripts/run-npx.sh /usr/local/bin/run-npx.sh
chmod +rx /usr/local/bin/run-npx.sh

cp ./scripts/run-rush.sh /usr/local/bin/run-rush.sh
chmod +rx /usr/local/bin/run-rush.sh
cp ./scripts/run-rush-pnpm.sh /usr/local/bin/run-rush-pnpm.sh
chmod +rx /usr/local/bin/run-rush-pnpm.sh

cp ./scripts/run-pnpm.sh /usr/local/bin/run-pnpm.sh
chmod +rx /usr/local/bin/run-pnpm.sh
cp ./scripts/run-pnpx.sh /usr/local/bin/run-pnpx.sh
chmod +rx /usr/local/bin/run-pnpx.sh
# Replace AZURE_DEVOPS_NUGET_FEED_URL_PREFIX in scripts that require it
sed -i "s|REPLACE_WITH_AZURE_DEVOPS_NUGET_FEED_URL_PREFIX|${PREFIXES}|g" ./scripts/dotnet
sed -i "s|REPLACE_WITH_AZURE_DEVOPS_NUGET_FEED_URL_PREFIX|${PREFIXES}|g" ./scripts/nuget

# Create ${SHIM_DIRECTORY}
mkdir -p "${SHIM_DIRECTORY}"

# Install helper scripts in ${SHIM_DIRECTORY}
cp "./scripts/auth-ado.sh" "${SHIM_DIRECTORY}"
cp "./scripts/resolve-shim.sh" "${SHIM_DIRECTORY}"
cp "./scripts/write-npm.sh" "${SHIM_DIRECTORY}"
chmod +rx "${SHIM_DIRECTORY}/write-npm.sh"

# Install selected shim scripts in ${SHIM_DIRECTORY}
for alias in "${ALIASES_ARR[@]}"; do
chmod +rx "./scripts/${alias}"
cp "./scripts/${alias}" "${SHIM_DIRECTORY}"
done

if [ "${INSTALL_PIP_HELPER}" = "true" ]; then
USER="${_REMOTE_USER}" /tmp/install-python-keyring.sh
Expand All @@ -126,16 +123,17 @@ fi

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

ALIASES_BLOCK=""
for ALIAS in "${ALIASES_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
ALIASES_BLOCK+="$ALIAS() { \"${SHIM_DIRECTORY}/$ALIAS\" \"\$@\"; }\n"
done

for TARGET_FILE in "${TARGET_FILES_ARR[@]}"; do
if [ "${INSTALL_WITH_SUDO}" = "true" ]; then
sudo -u ${_REMOTE_USER} bash -c "printf '%s' \"$ALIASES_BLOCK\" >> $TARGET_FILE"
else
printf '%s' "$ALIASES_BLOCK" >> "$TARGET_FILE" || true
fi
done

if [ "${INSTALL_WITH_SUDO}" = "true" ]; then
Expand Down
30 changes: 30 additions & 0 deletions src/artifacts-helper/scripts/auth-ado.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#!/bin/bash

echo "::step::Waiting for AzDO Authentication Helper..."

# Wait up to 3 minutes for the ado-auth-helper to be installed
# Can be overridden via environment variable for testing
MAX_WAIT=${MAX_WAIT:-180}
ELAPSED=0

while [ $ELAPSED -lt $MAX_WAIT ]; do
if [ -f "${HOME}/ado-auth-helper" ]; then
echo "::step::Running ado-auth-helper get-access-token..."
ARTIFACTS_ACCESSTOKEN=$(${HOME}/ado-auth-helper get-access-token)
echo "::step::✓ Access token retrieved successfully"
return 0
fi
sleep 2
ELAPSED=$((ELAPSED + 2))

# Progress indicator every 20 seconds
if [ $((ELAPSED % 20)) -eq 0 ]; then
echo " Still waiting... (${ELAPSED}s elapsed)"
fi
done

# Timeout reached - continue without authentication
echo "::warning::AzDO Authentication Helper not found after ${MAX_WAIT} seconds"
echo "Expected location: ${HOME}/ado-auth-helper"
echo "Continuing without Azure Artifacts authentication..."
return 1
11 changes: 11 additions & 0 deletions src/artifacts-helper/scripts/dotnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash
source "$(dirname $0)"/auth-ado.sh
source "$(dirname $0)"/resolve-shim.sh

# Install artifact credential provider if it is not already installed
if [ ! -d "${HOME}/.nuget/plugins/netcore" ]; then
wget -qO- https://aka.ms/install-artifacts-credprovider.sh | bash
fi

DOTNET_EXE="$(resolve_shim)"
VSS_NUGET_ACCESSTOKEN="${ARTIFACTS_ACCESSTOKEN:-}" VSS_NUGET_URI_PREFIXES=REPLACE_WITH_AZURE_DEVOPS_NUGET_FEED_URL_PREFIX ${DOTNET_EXE} "$@"
6 changes: 6 additions & 0 deletions src/artifacts-helper/scripts/npm
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
Copy link
Author

Choose a reason for hiding this comment

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

These all (apart from dotnet + nuget) could be auto-generated, since they are identical except in naming.

source "$(dirname $0)"/auth-ado.sh
source "$(dirname $0)"/resolve-shim.sh

NPM_EXE="$(resolve_shim)"
ARTIFACTS_ACCESSTOKEN="${ARTIFACTS_ACCESSTOKEN:-}" ${NPM_EXE} "$@"
6 changes: 6 additions & 0 deletions src/artifacts-helper/scripts/npx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
source "$(dirname $0)"/auth-ado.sh
source "$(dirname $0)"/resolve-shim.sh

NPX_EXE="$(resolve_shim)"
ARTIFACTS_ACCESSTOKEN="${ARTIFACTS_ACCESSTOKEN:-}" ${NPX_EXE} "$@"
11 changes: 11 additions & 0 deletions src/artifacts-helper/scripts/nuget
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/bin/bash
source "$(dirname $0)"/auth-ado.sh
source "$(dirname $0)"/resolve-shim.sh

# Install artifact credential provider if it is not already installed
if [ ! -d "${HOME}/.nuget/plugins/netcore" ]; then
wget -qO- https://aka.ms/install-artifacts-credprovider.sh | bash
fi

NUGET_EXE="$(resolve_shim)"
VSS_NUGET_ACCESSTOKEN="${ARTIFACTS_ACCESSTOKEN:-}" VSS_NUGET_URI_PREFIXES=REPLACE_WITH_AZURE_DEVOPS_NUGET_FEED_URL_PREFIX ${NUGET_EXE} "$@"
6 changes: 6 additions & 0 deletions src/artifacts-helper/scripts/pnpm
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
source "$(dirname $0)"/auth-ado.sh
source "$(dirname $0)"/resolve-shim.sh

PNPM_EXE="$(resolve_shim)"
ARTIFACTS_ACCESSTOKEN="${ARTIFACTS_ACCESSTOKEN:-}" ${PNPM_EXE} "$@"
6 changes: 6 additions & 0 deletions src/artifacts-helper/scripts/pnpx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
source "$(dirname $0)"/auth-ado.sh
source "$(dirname $0)"/resolve-shim.sh

PNPX_EXE="$(resolve_shim)"
ARTIFACTS_ACCESSTOKEN="${ARTIFACTS_ACCESSTOKEN:-}" ${PNPX_EXE} "$@"
22 changes: 22 additions & 0 deletions src/artifacts-helper/scripts/resolve-shim.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/bin/bash
[[ ${RESOLVE_SHIMS_IMPORTED} == "true" ]] && return
RESOLVE_SHIMS_IMPORTED=true
Comment on lines +2 to +3
Copy link
Author

Choose a reason for hiding this comment

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

Useful to avoid the function being redefined according to Copilot. Probably unnecessary.


resolve_shim() {
# Find the next non-shim executable in PATH so we do not run the shim again
shim_file="$(readlink -f "${BASH_SOURCE[1]}")"
executable="$(basename "$shim_file")"

# Read into array first to handle spaces properly
readarray -t candidates < <(which -a "$executable" 2>/dev/null)

for candidate in "${candidates[@]}"; do
# Skip any candidate which is a symlink to the shim file
[[ "$(readlink -f "$candidate")" != "$shim_file" ]] && {
echo "$candidate"
return 0
}
done

return 1
}
25 changes: 0 additions & 25 deletions src/artifacts-helper/scripts/run-dotnet.sh

This file was deleted.

18 changes: 0 additions & 18 deletions src/artifacts-helper/scripts/run-npm.sh

This file was deleted.

18 changes: 0 additions & 18 deletions src/artifacts-helper/scripts/run-npx.sh

This file was deleted.

Loading
Loading