├── .azure-devops ├── acr_image_purge.sh └── azure-pipelines.yml ├── .devcontainer ├── Dockerfile ├── devcontainer-lock.json ├── devcontainer.json └── scripts │ ├── act.sh │ ├── azure-cli.sh │ ├── docker-client.sh │ ├── gh.sh │ ├── node.sh │ └── postCreate.sh ├── .gitattributes ├── .github ├── CODEOWNERS ├── dependabot.yml ├── scripts │ ├── build.js │ ├── build.test.js │ ├── package-lock.json │ ├── package.json │ ├── test-helpers.js │ └── test-helpers.test.js └── workflows │ ├── ci_branch.yml │ ├── ci_common.yml │ ├── ci_main.yml │ ├── clean_tags.sh │ ├── clean_untagged.sh │ ├── codeql-analysis.yml │ ├── pr-bot.yml │ ├── pr-closed.yml │ ├── pr_auto.yml │ └── untagged-image-cleanup.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── action.yml ├── azdo-task ├── DevcontainersCi │ ├── .eslintignore │ ├── .eslintrc.json │ ├── .prettierignore │ ├── .prettierrc.json │ ├── Makefile │ ├── __tests__ │ │ └── exec.test.ts │ ├── jest.config.js │ ├── package-lock.json │ ├── package.json │ ├── run-main.js │ ├── run-post.js │ ├── src │ │ ├── docker.ts │ │ ├── exec.ts │ │ ├── main.ts │ │ └── skopeo.ts │ ├── task.json │ └── tsconfig.json ├── LICENSE.md ├── README.md ├── icon.png ├── package-lock.json ├── scripts │ ├── build-package.sh │ └── run-azdo-build.sh ├── tsconfig.json └── vss-extension.json ├── common ├── __tests__ │ ├── config.test.ts │ ├── docker.test.ts │ ├── envvars.test.ts │ └── users.test.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── src │ ├── config.ts │ ├── dev-container-cli.ts │ ├── docker.ts │ ├── envvars.ts │ ├── exec.ts │ ├── file.ts │ ├── skopeo.ts │ ├── users.ts │ └── windows.ts └── tsconfig.json ├── docs ├── azure-devops-task.md ├── github-action.md └── multi-platform-builds.md ├── github-action ├── .eslintignore ├── .eslintrc.json ├── .prettierignore ├── .prettierrc.json ├── Makefile ├── jest.config.js ├── package-lock.json ├── package.json ├── run-main.js ├── run-post.js ├── src │ ├── docker.ts │ ├── exec.ts │ ├── main.ts │ └── skopeo.ts └── tsconfig.json ├── github-tests ├── Dockerfile │ ├── build-args │ │ └── .devcontainer │ │ │ ├── Dockerfile │ │ │ ├── devcontainer.json │ │ │ └── scripts │ │ │ └── docker-client.sh │ ├── build-only │ │ └── .devcontainer │ │ │ ├── Dockerfile │ │ │ └── devcontainer.json │ ├── config-file │ │ └── .devcontainer │ │ │ └── subfolder │ │ │ └── devcontainer.json │ ├── docker-from-docker-non-root │ │ ├── .devcontainer │ │ │ ├── Dockerfile │ │ │ ├── devcontainer.json │ │ │ └── scripts │ │ │ │ ├── docker-client.sh │ │ │ │ └── golang.sh │ │ ├── Dockerfile │ │ ├── Makefile │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ └── scripts │ │ │ └── go-tools.sh │ ├── docker-from-docker-root │ │ ├── .devcontainer │ │ │ ├── Dockerfile │ │ │ ├── devcontainer.json │ │ │ └── scripts │ │ │ │ ├── docker-client.sh │ │ │ │ └── golang.sh │ │ ├── Dockerfile │ │ ├── Makefile │ │ ├── go.mod │ │ ├── go.sum │ │ ├── main.go │ │ └── scripts │ │ │ └── go-tools.sh │ ├── dockerfile-context │ │ ├── .devcontainer │ │ │ ├── CustomDockerfile │ │ │ └── devcontainer.json │ │ └── context │ │ │ └── dummy.sh │ ├── env-vars-on-post-create │ │ └── .devcontainer │ │ │ ├── Dockerfile │ │ │ ├── devcontainer.json │ │ │ └── post-create.sh │ ├── feature-docker-from-docker │ │ ├── .devcontainer │ │ │ ├── Dockerfile │ │ │ └── devcontainer.json │ │ ├── Dockerfile │ │ ├── Makefile │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── image-tag │ │ └── .devcontainer │ │ │ ├── Dockerfile │ │ │ ├── devcontainer.json │ │ │ └── scripts │ │ │ └── docker-client.sh │ ├── outputs │ │ └── .devcontainer.json │ ├── platform-with-runcmd │ │ └── .devcontainer │ │ │ ├── Dockerfile │ │ │ └── devcontainer.json │ ├── run-args │ │ └── .devcontainer │ │ │ ├── Dockerfile │ │ │ └── devcontainer.json │ └── skip-user-update │ │ └── .devcontainer │ │ ├── Dockerfile │ │ └── devcontainer.json └── docker-compose │ └── features │ └── .devcontainer │ ├── Dockerfile │ ├── devcontainer.json │ └── docker-compose.yml ├── maintainers.md └── scripts ├── build-local.sh ├── build-test-package.sh ├── get-latest-cli-build.sh ├── gh-release.sh ├── publish-azdo-task.sh └── test-azdo.sh /.azure-devops/acr_image_purge.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | 5 | function show_usage() { 6 | echo 7 | echo "acr_image_purge.sh" 8 | echo 9 | echo "Sets up auto-purging of images" 10 | echo 11 | echo -e "\t--registry-name\t(Required)The name of the ACR containing the images to protect/purge" 12 | echo 13 | } 14 | 15 | 16 | # Set default values here 17 | registry_name="" 18 | 19 | 20 | # Process switches: 21 | while [[ $# -gt 0 ]] 22 | do 23 | case "$1" in 24 | --registry-name) 25 | registry_name=$2 26 | shift 2 27 | ;; 28 | *) 29 | echo "Unexpected '$1'" 30 | show_usage 31 | exit 1 32 | ;; 33 | esac 34 | done 35 | 36 | 37 | if [[ -z $registry_name ]]; then 38 | echo "--registry-name must be specified" 39 | show_usage 40 | exit 1 41 | fi 42 | 43 | 44 | 45 | # Protect 'latest' tags to improve general build times 46 | image_names=$(az acr repository list --name $registry_name -o tsv) 47 | 48 | 49 | for image_name in ${image_names[@]}; 50 | do 51 | echo "Preventing delete for $image_name:latest" 52 | az acr repository update \ 53 | --name $registry_name \ 54 | --image $image_name:latest \ 55 | --delete-enabled false \ 56 | --write-enabled true 57 | done 58 | 59 | 60 | PURGE_CMD="acr purge --filter '.*:pr-[0-9a-fA-F]+' --ago 4d --untagged" 61 | 62 | az acr task create --name prTagPruneTask \ 63 | --cmd "$PURGE_CMD" \ 64 | --registry $registry_name \ 65 | --schedule "4 18 * * *" \ 66 | --context /dev/null \ 67 | --base-image-trigger-enabled false 68 | -------------------------------------------------------------------------------- /.azure-devops/azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | trigger: none 2 | 3 | pool: 4 | vmImage: ubuntu-latest 5 | 6 | jobs: 7 | - job: test_local 8 | displayName: Test local dev container definition 9 | steps: 10 | - script: | 11 | docker login -u $ACR_USERNAME -p $ACR_TOKEN $(ACR_NAME).azurecr.io 12 | displayName: 'Log in to Azure Container Registry' 13 | env: 14 | ACR_NAME: $(ACR_NAME) 15 | ACR_TOKEN: $(ACR_TOKEN) 16 | ACR_USERNAME: $(ACR_USERNAME) 17 | 18 | - script: | 19 | printenv | sort 20 | env: 21 | IMAGE_TAG: $(IMAGE_TAG) 22 | 23 | - task: DevcontainersCi@0 24 | inputs: 25 | imageName: '$(ACR_NAME).azurecr.io/devcontainers-ci/azdo-devcontainer-build-run-examples-acr' 26 | runCmd: 'echo $PWD' 27 | subFolder: '.' 28 | env: '' 29 | imageTag: $(IMAGE_TAG) 30 | sourceBranchFilterForPush: '' 31 | 32 | - job: test_run_args 33 | displayName: Test run-args 34 | steps: 35 | - script: | 36 | docker login -u $ACR_USERNAME -p $ACR_TOKEN $(ACR_NAME).azurecr.io 37 | displayName: 'Log in to Azure Container Registry' 38 | env: 39 | ACR_NAME: $(ACR_NAME) 40 | ACR_TOKEN: $(ACR_TOKEN) 41 | ACR_USERNAME: $(ACR_USERNAME) 42 | 43 | - task: DevcontainersCi@0 44 | inputs: 45 | imageName: '$(ACR_NAME).azurecr.io/devcontainers-ci/azdo-devcontainer-build-run/test/run-args' 46 | subFolder: github-tests/Dockerfile/run-args 47 | runCmd: echo $HOSTNAME && [[ $HOSTNAME == "my-host" ]] 48 | env: '' 49 | imageTag: $(IMAGE_TAG) 50 | sourceBranchFilterForPush: '' 51 | 52 | - job: test_env_vars_on_post_create 53 | displayName: Test env-vars-on-post-create 54 | steps: 55 | - script: | 56 | docker login -u $ACR_USERNAME -p $ACR_TOKEN $(ACR_NAME).azurecr.io 57 | displayName: 'Log in to Azure Container Registry' 58 | env: 59 | ACR_NAME: $(ACR_NAME) 60 | ACR_TOKEN: $(ACR_TOKEN) 61 | ACR_USERNAME: $(ACR_USERNAME) 62 | 63 | - task: DevcontainersCi@0 64 | env: # TEST_ENV_VALUE1 is set via devcontainer.json using a localEnv reference 65 | TEST_ENV_VALUE1: SetViaDevcontainerJsonLocalEnv 66 | inputs: 67 | imageName: '$(ACR_NAME).azurecr.io/devcontainers-ci/azdo-devcontainer-build-run/test/env-vars-on-post-create' 68 | subFolder: github-tests/Dockerfile/env-vars-on-post-create 69 | runCmd: | 70 | cat marker.txt 71 | cat marker.txt | grep 'post-create: TEST_ENV_VALUE1=SetViaDevcontainerJsonLocalEnv' 72 | cat marker.txt | grep 'post-create: TEST_ENV_VALUE2=AdditionalEnvVar' 73 | env: | # TEST_ENV_VALUE2 is an additional env var to pass to the container 74 | TEST_ENV_VALUE2=AdditionalEnvVar 75 | imageTag: $(IMAGE_TAG) 76 | sourceBranchFilterForPush: '' 77 | 78 | 79 | - job: test_simple 80 | displayName: Test simple 81 | steps: 82 | - task: DevcontainersCi@0 83 | inputs: 84 | subFolder: github-tests/Dockerfile/run-args 85 | runCmd: echo $HOSTNAME && [[ $HOSTNAME == "my-host" ]] 86 | - script: | 87 | echo "'runCmdOutput' value: $runCmdOutput" 88 | if [["$runCmdOutput" = *my-host*]]; then 89 | echo "'runCmdOutput' output of test_simple job doesn't contain expected value 'my-host'" 90 | exit 1 91 | fi 92 | 93 | - job: test_config_file 94 | displayName: Test configFile option 95 | steps: 96 | - task: DevcontainersCi@0 97 | inputs: 98 | subFolder: github-tests/Dockerfile/config-file 99 | configFile: github-tests/Dockerfile/config-file/.devcontainer/subfolder/devcontainer.json 100 | runCmd: echo $HOSTNAME && [[ $HOSTNAME == "my-host" ]] 101 | - script: | 102 | echo "'runCmdOutput' value: $runCmdOutput" 103 | if [["$runCmdOutput" = *my-host*]]; then 104 | echo "'runCmdOutput' output of test_config_file job doesn't contain expected value 'my-host'" 105 | exit 1 106 | fi 107 | 108 | - job: test_build_args 109 | displayName: Test build-args 110 | steps: 111 | - script: | 112 | docker login -u $ACR_USERNAME -p $ACR_TOKEN $(ACR_NAME).azurecr.io 113 | displayName: 'Log in to Azure Container Registry' 114 | env: 115 | ACR_NAME: $(ACR_NAME) 116 | ACR_TOKEN: $(ACR_TOKEN) 117 | ACR_USERNAME: $(ACR_USERNAME) 118 | 119 | - script: | 120 | printenv | sort 121 | env: 122 | IMAGE_TAG: $(IMAGE_TAG) 123 | 124 | - task: DevcontainersCi@0 125 | inputs: 126 | imageName: '$(ACR_NAME).azurecr.io/devcontainers-ci/azdo-devcontainer-build-run/test/build-args' 127 | subFolder: github-tests/Dockerfile/build-args 128 | runCmd: echo $BUILD_ARG_TEST && [[ $BUILD_ARG_TEST == "Hello build-args!" ]] 129 | env: '' 130 | imageTag: $(IMAGE_TAG) 131 | sourceBranchFilterForPush: '' 132 | 133 | - job: test_dockerfile_context 134 | displayName: Test Dockerfile context 135 | steps: 136 | - script: | 137 | docker login -u $ACR_USERNAME -p $ACR_TOKEN $(ACR_NAME).azurecr.io 138 | displayName: 'Log in to Azure Container Registry' 139 | env: 140 | ACR_NAME: $(ACR_NAME) 141 | ACR_TOKEN: $(ACR_TOKEN) 142 | ACR_USERNAME: $(ACR_USERNAME) 143 | 144 | - script: | 145 | printenv | sort 146 | env: 147 | IMAGE_TAG: $(IMAGE_TAG) 148 | 149 | - task: DevcontainersCi@0 150 | inputs: 151 | imageName: '$(ACR_NAME).azurecr.io/devcontainers-ci/azdo-devcontainer-build-run/test/dockerfile-context' 152 | subFolder: github-tests/Dockerfile/dockerfile-context 153 | runCmd: /tmp/dummy.sh 154 | env: '' 155 | imageTag: $(IMAGE_TAG) 156 | sourceBranchFilterForPush: '' 157 | 158 | - job: test_feature_docker_from_docker 159 | displayName: Test docker-from-docker using feature 160 | steps: 161 | - script: | 162 | docker login -u $ACR_USERNAME -p $ACR_TOKEN $(ACR_NAME).azurecr.io 163 | displayName: 'Log in to Azure Container Registry' 164 | env: 165 | ACR_NAME: $(ACR_NAME) 166 | ACR_TOKEN: $(ACR_TOKEN) 167 | ACR_USERNAME: $(ACR_USERNAME) 168 | 169 | - script: | 170 | printenv | sort 171 | env: 172 | IMAGE_TAG: $(IMAGE_TAG) 173 | 174 | - task: DevcontainersCi@0 175 | inputs: 176 | imageName: '$(ACR_NAME).azurecr.io/devcontainers-ci/azdo-devcontainer-build-run/test/feature-docker-from-docker' 177 | subFolder: github-tests/Dockerfile/feature-docker-from-docker 178 | runCmd: make docker-build 179 | env: '' 180 | imageTag: $(IMAGE_TAG) 181 | sourceBranchFilterForPush: '' 182 | 183 | - job: test_no_runCmd 184 | displayName: Test without runCmd 185 | steps: 186 | - task: DevcontainersCi@0 187 | inputs: 188 | subFolder: github-tests/Dockerfile/build-only 189 | 190 | - job: test_platform_with_runcmd 191 | displayName: Test with platform and runCmd 192 | steps: 193 | - script: | 194 | docker login -u $ACR_USERNAME -p $ACR_TOKEN $(ACR_NAME).azurecr.io 195 | displayName: 'Log in to Azure Container Registry' 196 | env: 197 | ACR_NAME: $(ACR_NAME) 198 | ACR_TOKEN: $(ACR_TOKEN) 199 | ACR_USERNAME: $(ACR_USERNAME) 200 | 201 | - script: | 202 | printenv | sort 203 | env: 204 | IMAGE_TAG: $(IMAGE_TAG) 205 | 206 | # This can be omitted once runner images have a version of Skopeo > 1.4.1 207 | - script: | 208 | sudo apt purge buildah golang-github-containers-common podman skopeo 209 | sudo apt autoremove --purge 210 | REPO_URL="https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable" 211 | source /etc/os-release 212 | sudo sh -c "echo 'deb ${REPO_URL}/x${NAME}_${VERSION_ID}/ /' > /etc/apt/sources.list.d/devel:kubic:libcontainers:unstable.list" 213 | sudo wget -qnv https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable/x${NAME}_${VERSION_ID}/Release.key -O Release.key 214 | sudo apt-key add Release.key 215 | sudo apt-get update 216 | sudo apt-get install skopeo 217 | displayName: Update skopeo 218 | 219 | 220 | - script: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 221 | displayName: Set up QEMU 222 | 223 | - script: docker buildx create --use 224 | displayName: Set up docker buildx 225 | 226 | - task: DevcontainersCi@0 227 | inputs: 228 | imageName: '$(ACR_NAME).azurecr.io/devcontainers-ci/azdo-devcontainer-build-run/test/platform-with-runcmd' 229 | subFolder: github-tests/Dockerfile/platform-with-runcmd 230 | platform: linux/amd64,linux/arm64 231 | runCmd: echo $HOSTNAME && [[ $HOSTNAME == "my-host" ]] 232 | 233 | - script: | 234 | echo "'runCmdOutput' value: $runCmdOutput" 235 | if [["$runCmdOutput" = *my-host*]]; then 236 | echo "'runCmdOutput' output of test_simple job doesn't contain expected value 'my-host'" 237 | exit 1 238 | fi -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian / Ubuntu version: debian-10, debian-9, ubuntu-20.04, ubuntu-18.04 2 | # See https://github.com/microsoft/vscode-dev-containers/tree/master/containers/debian 3 | ARG VARIANT=debian-12 4 | FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT} 5 | 6 | 7 | # Avoid warnings by switching to noninteractive 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | 10 | # Set env for tracking that we're running in a devcontainer 11 | ENV DEVCONTAINER=true 12 | 13 | # This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" 14 | # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs 15 | # will be updated to match your local UID/GID (when using the dockerFile property). 16 | # See https://aka.ms/vscode-remote/containers/non-root-user for details. 17 | ARG USERNAME=vscode 18 | ARG USER_UID=1000 19 | ARG USER_GID=$USER_UID 20 | 21 | USER $USERNAME 22 | RUN \ 23 | mkdir -p ~/.local/bin \ 24 | && echo "export PATH=\$PATH:~/.local/bin" >> ~/.bashrc 25 | 26 | # Configure apt, install packages and general tools 27 | RUN sudo apt-get update \ 28 | && sudo apt-get -y install --no-install-recommends apt-utils dialog nano bash-completion sudo bsdmainutils \ 29 | # 30 | # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed 31 | && sudo apt-get -y install git iproute2 procps lsb-release figlet build-essential 32 | 33 | # Save command line history 34 | RUN echo "export HISTFILE=/home/$USERNAME/commandhistory/.bash_history" >> "/home/$USERNAME/.bashrc" \ 35 | && echo "export PROMPT_COMMAND='history -a'" >> "/home/$USERNAME/.bashrc" \ 36 | && mkdir -p /home/$USERNAME/commandhistory \ 37 | && touch /home/$USERNAME/commandhistory/.bash_history \ 38 | && chown -R $USERNAME /home/$USERNAME/commandhistory 39 | 40 | # Set env for tracking that we're running in a devcontainer 41 | ENV DEVCONTAINER=true 42 | 43 | # node 44 | COPY scripts/node.sh /tmp/ 45 | RUN /bin/bash /tmp/node.sh 20.x 46 | 47 | # docker-client 48 | COPY scripts/docker-client.sh /tmp/ 49 | RUN /bin/bash /tmp/docker-client.sh 50 | 51 | #Add user to docker group 52 | RUN sudo groupadd docker && sudo usermod -aG docker $USERNAME && newgrp docker 53 | 54 | # act 55 | COPY scripts/act.sh /tmp/ 56 | RUN /bin/bash /tmp/act.sh 0.2.21 57 | 58 | RUN sudo npm install -g tfx-cli 59 | 60 | # azure-cli-no-mount 61 | COPY scripts/azure-cli.sh /tmp/ 62 | RUN /bin/bash /tmp/azure-cli.sh 63 | 64 | RUN az extension add --name azure-devops 65 | 66 | # __DEVCONTAINER_SNIPPET_INSERT__ (control where snippets get inserted using the devcontainer CLI) 67 | 68 | # Switch back to dialog for any ad-hoc use of apt-get 69 | ENV DEBIAN_FRONTEND=dialog 70 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "features": { 3 | "ghcr.io/devcontainers/features/github-cli:1": { 4 | "version": "1.0.14", 5 | "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:ca677566507c4118e4368cd76a4800807e911e5e350cc3525fb67ebc9132dfa1", 6 | "integrity": "sha256:ca677566507c4118e4368cd76a4800807e911e5e350cc3525fb67ebc9132dfa1" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "devcontainers-ci", 5 | "dockerFile": "Dockerfile", 6 | "build": { 7 | "cacheFrom": "ghcr.io/devcontainers/ci-devcontainer:latest" 8 | }, 9 | "mounts": [ 10 | // Keep command history 11 | "source=devcontainer-build-run-bashhistory,target=/home/vscode/commandhistory", 12 | // Mount host docker socket 13 | "type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock" 14 | ], 15 | "postCreateCommand": ".devcontainer/scripts/postCreate.sh", 16 | "remoteUser": "vscode", 17 | "customizations": { 18 | "vscode": { 19 | "settings": { 20 | "terminal.integrated.defaultProfile.linux": "bash", 21 | "files.eol": "\n" 22 | }, 23 | "extensions": [ 24 | "ms-azuretools.vscode-docker", 25 | "yzhang.markdown-all-in-one", 26 | "davidanson.vscode-markdownlint", 27 | "heaths.vscode-guid", 28 | "esbenp.prettier-vscode", 29 | "meganrogge.template-string-converter", 30 | "ms-azure-devops.azure-pipelines" 31 | ] 32 | } 33 | }, 34 | "features": { 35 | "ghcr.io/devcontainers/features/github-cli:1": "latest" 36 | } 37 | } -------------------------------------------------------------------------------- /.devcontainer/scripts/act.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | get_latest_release() { 5 | curl --silent "https://api.github.com/repos/$1/releases/latest" | 6 | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/' 7 | } 8 | 9 | VERSION=${1:-"$(get_latest_release nektos/act)"} 10 | INSTALL_DIR=${2:-"$HOME/.local/bin"} 11 | CMD=act 12 | NAME="nektos/act" 13 | 14 | echo -e "\e[34m»»» 📦 \e[32mInstalling \e[33m$NAME v$VERSION\e[0m ..." 15 | 16 | mkdir -p $INSTALL_DIR 17 | curl -sSL https://github.com/nektos/act/releases/download/v${VERSION}/act_Linux_x86_64.tar.gz -o /tmp/act.tar.gz 18 | tar -zxvf /tmp/act.tar.gz -C $INSTALL_DIR act > /dev/null 19 | chmod +x $INSTALL_DIR/act 20 | rm -rf /tmp/act.tar.gz 21 | 22 | echo -e "\n\e[34m»»» 💾 \e[32mInstalled to: \e[33m$(which $CMD)" 23 | echo -e "\e[34m»»» 💡 \e[32mVersion details: \e[39m$($CMD --version)" -------------------------------------------------------------------------------- /.devcontainer/scripts/azure-cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | CMD=az 5 | NAME="Azure CLI" 6 | 7 | echo -e "\e[34m»»» 📦 \e[32mInstalling \e[33m$NAME\e[0m ..." 8 | 9 | curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash 10 | 11 | echo -e "\n\e[34m»»» 💾 \e[32mInstalled to: \e[33m$(which $CMD)" 12 | echo -e "\e[34m»»» 💡 \e[32mVersion details: \e[39m$($CMD --version)" -------------------------------------------------------------------------------- /.devcontainer/scripts/docker-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | VERSION=${1:-"20.10.5"} 5 | INSTALL_DIR=${2:-"$HOME/.local/bin"} 6 | CMD=docker 7 | NAME="Docker Client" 8 | 9 | echo -e "\e[34m»»» 📦 \e[32mInstalling \e[33m$NAME \e[35mv$VERSION\e[0m ..." 10 | 11 | mkdir -p $INSTALL_DIR 12 | curl -sSL https://download.docker.com/linux/static/stable/x86_64/docker-$VERSION.tgz -o /tmp/docker.tgz 13 | tar -zxvf /tmp/docker.tgz -C /tmp docker/docker 14 | chmod +x /tmp/docker/docker 15 | mv /tmp/docker/docker $INSTALL_DIR/docker 16 | rmdir /tmp/docker/ 17 | rm -rf /tmp/docker.tgz 18 | 19 | echo -e "\n\e[34m»»» 💾 \e[32mInstalled to: \e[33m$(which $CMD)" 20 | echo -e "\e[34m»»» 💡 \e[32mVersion details: \e[39m$($CMD --version)" 21 | -------------------------------------------------------------------------------- /.devcontainer/scripts/gh.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | get_latest_release() { 5 | curl --silent "https://api.github.com/repos/$1/releases/latest" | 6 | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/' 7 | } 8 | 9 | VERSION=${1:-"$(get_latest_release cli/cli)"} 10 | INSTALL_DIR=${2:-"$HOME/.local/bin"} 11 | CMD=gh 12 | NAME="GitHub CLI" 13 | 14 | echo -e "\e[34m»»» 📦 \e[32mInstalling \e[33m$NAME \e[35mv$VERSION\e[0m ..." 15 | 16 | mkdir -p $INSTALL_DIR 17 | curl -sSL https://github.com/cli/cli/releases/download/v${VERSION}/gh_${VERSION}_linux_amd64.tar.gz -o /tmp/gh.tar.gz 18 | tar -zxvf /tmp/gh.tar.gz --strip-components 2 -C $INSTALL_DIR gh_${VERSION}_linux_amd64/bin/gh > /dev/null 19 | chmod +x $INSTALL_DIR/gh 20 | rm -rf /tmp/gh.tar.gz 21 | 22 | echo -e "\n\e[34m»»» 💾 \e[32mInstalled to: \e[33m$(which $CMD)" 23 | echo -e "\e[34m»»» 💡 \e[32mVersion details: \e[39m$($CMD --version)" 24 | -------------------------------------------------------------------------------- /.devcontainer/scripts/node.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | VERSION=${1:-"20.x"} 3 | CMD=node 4 | NAME="Node.js" 5 | 6 | echo -e "\e[34m»»» 📦 \e[32mInstalling \e[33m$NAME \e[35mv$VERSION\e[0m ..." 7 | 8 | curl -sL https://deb.nodesource.com/setup_$VERSION | sudo -E bash - 9 | sudo apt install -y nodejs 10 | 11 | echo -e "\n\e[34m»»» 💾 \e[32mInstalled to: \e[33m$(which $CMD)" 12 | echo -e "\e[34m»»» 💡 \e[32mNode version details: \e[39m$($CMD --version)" 13 | echo -e "\e[34m»»» 💡 \e[32mNPM version details: \e[39m$(npm --version)" 14 | -------------------------------------------------------------------------------- /.devcontainer/scripts/postCreate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | sudo npm install --location=global yarn 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/** -diff linguist-generated=true -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @devcontainers/maintainers @stuartleeks 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "devcontainers" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | 13 | - package-ecosystem: "npm" 14 | directories: 15 | - "/azdo-task/DevcontainersCi" 16 | - "/common" 17 | - "/github-action" 18 | - "/.github/scripts" 19 | groups: 20 | all: 21 | patterns: 22 | - "*" 23 | ignore: 24 | - dependency-name: "eslint" 25 | update-types: ["version-update:semver-major"] # eslint 9 removed support for JSON config files 26 | schedule: 27 | interval: "weekly" 28 | -------------------------------------------------------------------------------- /.github/scripts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "build-scripts", 3 | "version": "1.0.0", 4 | "description": "Scripts used in the GitHub workflows", 5 | "main": "build.js", 6 | "license": "MIT", 7 | "devDependencies": { 8 | "jest": "^29.7.0" 9 | }, 10 | "scripts": { 11 | "test": "jest --verbose", 12 | "test-watch": "jest --watchAll --verbose" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/scripts/test-helpers.js: -------------------------------------------------------------------------------- 1 | function lastCallFor(mock, matcher) { 2 | const matches = mock.mock.calls.filter(matcher); 3 | const lastMatchIndex = matches.length - 1; 4 | if (lastMatchIndex < 0) { 5 | return null; 6 | } 7 | return matches[lastMatchIndex]; 8 | } 9 | 10 | // outputFor returns the last call to mock with the specified key 11 | function outputFor(mock, key) { 12 | // get last call for mock with first arg matching the key 13 | const lastCall = lastCallFor(mock, c => c[0] === key); 14 | if (lastCall) { 15 | // return second arg (output value) 16 | return lastCall[1]; 17 | } 18 | return null; 19 | } 20 | 21 | function toHaveComment(received, { owner, repo, issue_number, bodyMatcher }) { 22 | const commentsForIssue = received.mock.calls 23 | .map(c => c[0]) 24 | .filter(arg => arg.owner === owner && arg.repo === repo && arg.issue_number === issue_number); 25 | if (commentsForIssue.length === 0) { 26 | return { message: () => `No comments found for issue ${JSON.stringify({ owner, repo, issue_number })}`, pass: false }; 27 | } 28 | 29 | const gotMatchingBody = commentsForIssue.some(c => bodyMatcher.exec(c.body) !== null) 30 | if (!gotMatchingBody) { 31 | const lastComment = commentsForIssue[commentsForIssue.length - 1] 32 | return { message: () => `No comment found with body matching ${bodyMatcher.toString()}. Last comment for issue: '${lastComment.body}'`, pass: false }; 33 | } 34 | return { message: '', pass: true }; 35 | } 36 | 37 | const PR_NUMBER = { 38 | UPSTREAM_NON_DOCS_CHANGES: 123, 39 | UPSTREAM_DOCS_ONLY_CHANGES: 125, 40 | FORK_NON_DOCS_CHANGES: 456, 41 | UPSTREAM_NON_MERGEABLE: 600, 42 | } 43 | 44 | function createGitHubContext() { 45 | mockGithubRestIssuesAddLabels = jest.fn(); 46 | mockGithubRestIssuesCreateComment = jest.fn(); 47 | mockGithubRestPullsListFiles = jest.fn(); 48 | mockCoreSetOutput = jest.fn(); 49 | mockCoreNotice = jest.fn(); 50 | mockCoreInfo = jest.fn(); 51 | mockCoreWarning = jest.fn(); 52 | mockCoreError = jest.fn(); 53 | return { 54 | mockGithubRestIssuesAddLabels, 55 | mockGithubRestIssuesCreateComment, 56 | mockCoreSetOutput, 57 | mockCoreInfo, 58 | mockCoreNotice, 59 | mockCoreWarning, 60 | mockCoreError, 61 | core: { 62 | setOutput: mockCoreSetOutput, 63 | info: mockCoreInfo, 64 | notice: mockCoreNotice, 65 | warning: mockCoreWarning, 66 | error: mockCoreError, 67 | }, 68 | github: { 69 | paginate: (func, params) => { 70 | // thin fake for paginate -> faked function being paginated should return a single page of data! 71 | // if you're getting a `func is not a function` error then check that a func is being passed in 72 | return func(params); 73 | }, 74 | request: async (route, data) => { 75 | if (route === 'GET /repos/{owner}/{repo}/collaborators/{username}') { 76 | if (data.username === "admin") { 77 | return { 78 | status: 204 79 | }; 80 | } else { 81 | throw { 82 | status: 404, 83 | }; 84 | } 85 | } 86 | }, 87 | rest: { 88 | issues: { 89 | addLabels: mockGithubRestIssuesAddLabels, 90 | createComment: mockGithubRestIssuesCreateComment, 91 | }, 92 | pulls: { 93 | get: async (params) => { 94 | if (params.owner === 'someOwner' 95 | && params.repo === 'someRepo') { 96 | switch (params.pull_number) { 97 | case PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES: // PR from the upstream repo with non-docs changes 98 | return { 99 | data: { 100 | head: { 101 | ref: 'pr-head-ref', 102 | sha: '0123456789', 103 | repo: { full_name: 'someOwner/someRepo' }, 104 | }, 105 | merge_commit_sha: '123456789a', 106 | mergeable: true, 107 | }, 108 | } 109 | case PR_NUMBER.UPSTREAM_DOCS_ONLY_CHANGES: // PR from the upstream repo with docs-only changes 110 | return { 111 | data: { 112 | head: { 113 | ref: 'pr-head-ref', 114 | sha: '0123456789', 115 | repo: { full_name: 'someOwner/someRepo' }, 116 | }, 117 | merge_commit_sha: '123456789a', 118 | mergeable: true, 119 | }, 120 | } 121 | case PR_NUMBER.FORK_NON_DOCS_CHANGES: // PR from a forked repo 122 | return { 123 | data: { 124 | head: { 125 | ref: 'pr-head-ref', 126 | sha: '23456789ab', 127 | repo: { full_name: 'anotherOwner/someRepo' }, 128 | }, 129 | merge_commit_sha: '3456789abc', 130 | mergeable: true, 131 | }, 132 | } 133 | case PR_NUMBER.UPSTREAM_NON_MERGEABLE: // PR with mergable==false 134 | return { 135 | data: { 136 | head: { 137 | ref: 'pr-head-ref', 138 | sha: '23456789ab', 139 | repo: { full_name: 'anotherOwner/someRepo' }, 140 | }, 141 | merge_commit_sha: '3456789abc', 142 | mergeable: false, 143 | }, 144 | } 145 | } 146 | } 147 | throw 'Unhandled params in fake pulls.get: ' + JSON.stringify(params) 148 | }, 149 | listFiles: async (params) => { 150 | if (params.owner === 'someOwner' 151 | && params.repo === 'someRepo') { 152 | switch (params.pull_number) { 153 | case PR_NUMBER.UPSTREAM_NON_DOCS_CHANGES: 154 | case PR_NUMBER.FORK_NON_DOCS_CHANGES: 155 | case PR_NUMBER.UPSTREAM_NON_MERGEABLE: 156 | return [{ filename: 'test.py' }, { filename: 'test.md' }]; 157 | 158 | case PR_NUMBER.UPSTREAM_DOCS_ONLY_CHANGES: 159 | return [{ filename: 'test.md' }, { filename: 'docs/some-image.png' }]; 160 | } 161 | } 162 | throw 'Unhandled params in fake pulls.listFiles: ' + JSON.stringify(params) 163 | } 164 | }, 165 | }, 166 | } 167 | }; 168 | } 169 | 170 | 171 | module.exports = { 172 | lastCallFor, 173 | outputFor, 174 | toHaveComment, 175 | createGitHubContext, 176 | PR_NUMBER, 177 | } 178 | -------------------------------------------------------------------------------- /.github/scripts/test-helpers.test.js: -------------------------------------------------------------------------------- 1 | const { lastCallFor, outputFor, toHaveComment } = require('./test-helpers.js') 2 | 3 | describe('helper tests', () => { 4 | 5 | describe('lastCallFor', () => { 6 | var testMock; 7 | beforeEach(() => { 8 | testMock = jest.fn(); 9 | }); 10 | 11 | test('single call matches', () => { 12 | testMock('key', 'value'); 13 | expect(lastCallFor(testMock, c => c[0] === 'key')[1]).toBe('value'); 14 | }) 15 | test('multiple calls matches last value', () => { 16 | testMock('key', 'value'); 17 | testMock('key', 'value2'); 18 | expect(lastCallFor(testMock, c => c[0] === 'key')[1]).toBe('value2'); 19 | }) 20 | }) 21 | 22 | describe('outputFor', () => { 23 | var setOutput; 24 | beforeEach(() => { 25 | setOutput = jest.fn(); 26 | }); 27 | 28 | test('no calls returns null', () => { 29 | expect(outputFor(setOutput, 'key')).toBe(null); 30 | }) 31 | test('single call matches', () => { 32 | setOutput('key', 'value'); 33 | expect(outputFor(setOutput, 'key')).toBe('value'); 34 | }) 35 | test('multiple calls matches last value', () => { 36 | setOutput('key', 'value'); 37 | setOutput('key', 'value2'); 38 | expect(outputFor(setOutput, 'key')).toBe('value2'); 39 | }) 40 | }) 41 | 42 | describe('toHaveComment', () => { 43 | expect.extend({ 44 | toHaveComment 45 | }) 46 | 47 | var add_comment; 48 | beforeEach(() => { 49 | add_comment = jest.fn(); 50 | }); 51 | 52 | test('no calls returns false', () => { 53 | expect(add_comment).not.toHaveComment({ 54 | owner: 'someOwner', 55 | repo: 'someRepo', 56 | issue_number: 123, 57 | bodyMatcher: /test/, 58 | }); 59 | }); 60 | 61 | test('single matching call returns true', () => { 62 | add_comment({ 63 | owner: 'someOwner', 64 | repo: 'someRepo', 65 | issue_number: 123, 66 | body: 'This is a test', 67 | }) 68 | expect(add_comment).toHaveComment({ 69 | owner: 'someOwner', 70 | repo: 'someRepo', 71 | issue_number: 123, 72 | bodyMatcher: /test/, 73 | }); 74 | }); 75 | 76 | test('single non-matching call returns false', () => { 77 | add_comment({ 78 | owner: 'someOwner', 79 | repo: 'someRepo', 80 | issue_number: 1234, 81 | body: 'This is a test', 82 | }) 83 | expect(add_comment).not.toHaveComment({ 84 | owner: 'someOwner', 85 | repo: 'someRepo', 86 | issue_number: 123, 87 | bodyMatcher: /test/, 88 | }); 89 | }); 90 | 91 | test('multiple calls, none matching, returns false', () => { 92 | add_comment({ 93 | owner: 'someOwner', 94 | repo: 'someRepo', 95 | issue_number: 1234, 96 | body: 'This is a test', 97 | }) 98 | add_comment({ 99 | owner: 'someOtherOwner', 100 | repo: 'someRepo', 101 | issue_number: 123, 102 | body: 'This is a test', 103 | }) 104 | add_comment({ 105 | owner: 'someOwner', 106 | repo: 'someOtherRepo', 107 | issue_number: 123, 108 | body: 'This is a test', 109 | }) 110 | add_comment({ 111 | owner: 'someOwner', 112 | repo: 'someRepo', 113 | issue_number: 1234, 114 | body: 'This is a test', 115 | }) 116 | expect(add_comment).not.toHaveComment({ 117 | owner: 'someOwner', 118 | repo: 'someRepo', 119 | issue_number: 123, 120 | bodyMatcher: /test/, 121 | }); 122 | }); 123 | 124 | 125 | test('multiple calls, with matching, returns true', () => { 126 | add_comment({ 127 | owner: 'someOwner', 128 | repo: 'someRepo', 129 | issue_number: 1234, 130 | body: 'This is a test', 131 | }) 132 | add_comment({ 133 | owner: 'someOtherOwner', 134 | repo: 'someRepo', 135 | issue_number: 123, 136 | body: 'This is a test', 137 | }) 138 | add_comment({ 139 | owner: 'someOwner', 140 | repo: 'someRepo', 141 | issue_number: 123, 142 | body: 'This is a test', 143 | }) 144 | add_comment({ 145 | owner: 'someOwner', 146 | repo: 'someOtherRepo', 147 | issue_number: 123, 148 | body: 'This is a test', 149 | }) 150 | expect(add_comment).toHaveComment({ 151 | owner: 'someOwner', 152 | repo: 'someRepo', 153 | issue_number: 123, 154 | bodyMatcher: /test/, 155 | }); 156 | }); 157 | 158 | 159 | }) 160 | 161 | }) 162 | -------------------------------------------------------------------------------- /.github/workflows/ci_branch.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI (branch) 3 | # This workflow is triggered manually and can be used to test workflow changes on a branch (which aren't picked up by the pr-bot builds) 4 | 5 | on: 6 | workflow_dispatch: 7 | inputs: 8 | prNumber: 9 | description: set to the PR number if testing a branch for a PR (used to derive the image tag) 10 | default: '' 11 | type: string 12 | required: false 13 | runFullTests: 14 | description: tick to run the full tests, untick to run tests without credentials 15 | default: true 16 | type: boolean 17 | required: false 18 | 19 | permissions: 20 | contents: write 21 | packages: write 22 | pull-requests: write 23 | checks: write 24 | 25 | jobs: 26 | build-test-publish: 27 | name: "Build, test, publish" 28 | uses: ./.github/workflows/ci_common.yml 29 | with: 30 | release: false 31 | prNumber: ${{ github.event.inputs.prNumber }} 32 | # despite the runFullTests input being a boolean, we seem to get a string below! 33 | runFullTests: ${{ github.event.inputs.runFullTests == 'true' }} 34 | secrets: 35 | AZDO_TOKEN: ${{ secrets.AZDO_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/ci_main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI (main) 3 | # This workflow is triggered on pushes to main and runs tests and performs the release steps 4 | 5 | on: 6 | push: 7 | branches: [main] 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | packages: write 13 | pull-requests: write 14 | checks: write 15 | 16 | jobs: 17 | build-test-publish: 18 | name: "Build, test, publish" 19 | if: github.ref == 'refs/heads/main' 20 | uses: ./.github/workflows/ci_common.yml 21 | with: 22 | release: true 23 | secrets: 24 | AZDO_TOKEN: ${{ secrets.AZDO_TOKEN }} 25 | -------------------------------------------------------------------------------- /.github/workflows/clean_tags.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | 5 | function show_usage() { 6 | echo 7 | echo "clean_tags.sh" 8 | echo 9 | echo "Clean tags across images" 10 | echo 11 | echo -e "\t--tag\t(Required)The tag to clean" 12 | echo 13 | } 14 | 15 | 16 | # Set default values here 17 | tag="" 18 | 19 | # Process switches: 20 | while [[ $# -gt 0 ]] 21 | do 22 | case "$1" in 23 | --tag) 24 | tag=$2 25 | shift 2 26 | ;; 27 | *) 28 | echo "Unexpected '$1'" 29 | show_usage 30 | exit 1 31 | ;; 32 | esac 33 | done 34 | 35 | 36 | if [[ -z $tag ]]; then 37 | echo "--tag" 38 | show_usage 39 | exit 1 40 | fi 41 | 42 | image_names=( 43 | "ci-devcontainer" 44 | "ci/tests/run-args" 45 | "ci/tests/build-args" 46 | "ci/tests/dockerfile-context" 47 | "ci/tests/feature-docker-from-docker" 48 | "ci/tests/docker-from-docker-non-root" 49 | "ci/tests/docker-from-docker-root" 50 | "ci/tests/skip-user-update" 51 | ) 52 | 53 | echo "Checking for tag $tag:" 54 | 55 | for image_name in ${image_names[@]}; 56 | do 57 | echo "Checking image $image_name..." 58 | escaped_image_name=$(echo ${image_name} | sed "s/\//%2f/g") 59 | response=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github.v3+json" "https://api.github.com/orgs/devcontainers/packages/container/$escaped_image_name/versions") 60 | message=$(echo "$response" | jq -r ".message?") 61 | if [[ -z "$message" || "$message" == "null" ]]; then 62 | version_id=$(echo "$response" | jq -r ".[] | select(.metadata.container.tags | index(\"${tag}\")) | .id") 63 | if [[ -n $version_id ]]; then 64 | echo "Found version '$version_id' for '$image_name:$tag' - deleting..." 65 | curl -s -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github.v3+json" "https://api.github.com/orgs/devcontainers/packages/container/$escaped_image_name/versions/$version_id" 66 | else 67 | echo "Tag '$tag' not found for '$image_name:$tag' - skipping" 68 | fi 69 | else 70 | echo "Error: $message" 71 | fi 72 | done 73 | 74 | 75 | echo "Done." -------------------------------------------------------------------------------- /.github/workflows/clean_untagged.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | 5 | image_names=( 6 | "ci-devcontainer" 7 | "ci/tests/run-args" 8 | "ci/tests/build-args" 9 | "ci/tests/dockerfile-context" 10 | "ci/tests/feature-docker-from-docker" 11 | "ci/tests/docker-from-docker-non-root" 12 | "ci/tests/docker-from-docker-root" 13 | "ci/tests/skip-user-update" 14 | ) 15 | 16 | for image_name in ${image_names[@]}; 17 | do 18 | echo "Checking for untagged versions for $image_name" 19 | escaped_image_name=$(echo ${image_name} | sed "s/\//%2f/g") 20 | response=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github.v3+json" "https://api.github.com/orgs/devcontainers/packages/container/$escaped_image_name/versions?per_page=100") 21 | message=$(echo "$response" | jq -r ".message?") 22 | if [[ -z "$message" || "$message" == "null" ]]; then 23 | version_ids=$(echo "$response" | jq -r ".[] | select(.metadata.container.tags | length ==0) | .id") 24 | for version_id in ${version_ids[@]}; 25 | do 26 | echo -e "\tDeleting version '$version_id' for '$image_name:$tag' ..." 27 | curl -s -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" -H "Accept: application/vnd.github.v3+json" "https://api.github.com/orgs/devcontainers/packages/container/$escaped_image_name/versions/$version_id" 28 | done 29 | else 30 | echo "Error: $message" 31 | fi 32 | done 33 | 34 | echo "Done." 35 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '41 9 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 37 | # Learn more: 38 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 39 | 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v4 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.github/workflows/pr-bot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: pr-bot 3 | # This workflow is triggered on PR comments 4 | 5 | 6 | on: 7 | issue_comment: 8 | types: [created] # only run on new comments 9 | # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment 10 | # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#issue_comment 11 | 12 | permissions: 13 | contents: write 14 | packages: write 15 | pull-requests: write 16 | checks: write 17 | 18 | jobs: 19 | pr_comment: 20 | name: PR comment 21 | # https://docs.github.com/en/graphql/reference/enums#commentauthorassociation 22 | # (and https://docs.github.com/en/rest/reference/issues#comments) 23 | 24 | # only allow commands where: 25 | # - the comment is on a PR 26 | # - the commenting user has write permissions (i.e. is OWNER or COLLABORATOR) 27 | if: ${{ github.event.issue.pull_request }} 28 | runs-on: ubuntu-latest 29 | outputs: 30 | command: ${{ steps.check_command.outputs.command }} 31 | prRef: ${{ steps.check_command.outputs.prRef }} 32 | prNumber: ${{ steps.check_command.outputs.prNumber }} 33 | prHeadSha: ${{ steps.check_command.outputs.prHeadSha }} 34 | steps: 35 | # Ensure we have the script file for the github-script action to use 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | with: 39 | persist-credentials: false 40 | 41 | # Determine whether the comment is a command 42 | - id: check_command 43 | name: Check for a command using GitHub script 44 | uses: actions/github-script@v7 45 | with: 46 | script: | 47 | const script = require('./.github/scripts/build.js') 48 | await script.getCommandFromComment({core, context, github}); 49 | 50 | - name: Output PR details 51 | run: | 52 | echo "PR Details" 53 | echo "==========" 54 | echo "command : ${{ steps.check_command.outputs.command }}" 55 | echo "prNumber : ${{ steps.check_command.outputs.prNumber }}" 56 | echo "prRef : ${{ steps.check_command.outputs.prRef }}" 57 | echo "prHeadSha : ${{ steps.check_command.outputs.prHeadSha }}" 58 | 59 | # If we are skipping the tests and don't run the actual deploy (see the run_test job below) 60 | # we won't receive a check-run status, and will have to send it "manually" 61 | - name: Bypass E2E check-runs status 62 | if: ${{ steps.check_command.outputs.command == 'test-force-approve' }} 63 | uses: LouisBrunner/checks-action@v2.0.0 64 | with: 65 | token: ${{ secrets.GITHUB_TOKEN }} 66 | # the name must be identical to the one received by the real job 67 | sha: ${{ steps.check_command.outputs.prHeadSha }} 68 | name: "Build, test, publish / All succeeded" 69 | status: "completed" 70 | conclusion: "success" 71 | 72 | run_test: 73 | needs: [pr_comment] 74 | if: ${{ needs.pr_comment.outputs.command == 'run-tests' }} 75 | name: build-test 76 | uses: ./.github/workflows/ci_common.yml 77 | with: 78 | prNumber: ${{ needs.pr_comment.outputs.prNumber }} 79 | prRef: ${{ needs.pr_comment.outputs.prRef }} 80 | prHeadSha: ${{ needs.pr_comment.outputs.prHeadSha }} 81 | release: false 82 | secrets: 83 | AZDO_TOKEN: ${{ secrets.AZDO_TOKEN }} 84 | -------------------------------------------------------------------------------- /.github/workflows/pr-closed.yml: -------------------------------------------------------------------------------- 1 | name: pr_closed 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | 7 | # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#issue_comment 8 | # https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#pull_request 9 | 10 | jobs: 11 | 12 | pr_closed: 13 | name: PR Closed 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | 21 | - name: Clean GitHub container images for PR 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | PR_IMAGE_TAG: pr-${{ github.event.pull_request.number }} 25 | run: 26 | ./.github/workflows/clean_tags.sh --tag $PR_IMAGE_TAG 27 | -------------------------------------------------------------------------------- /.github/workflows/pr_auto.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: pr_auto 3 | # This workflow is triggered automatically for pull requests and runs tests without credentials 4 | # I.e. doesn't validate image caching, doesn't perform tests in Azure DevOps. 5 | 6 | on: 7 | pull_request: 8 | branches: [ main ] 9 | paths-ignore: 10 | - README.md 11 | - 'docs/**' 12 | 13 | permissions: 14 | contents: write 15 | packages: write 16 | pull-requests: write 17 | checks: write 18 | 19 | jobs: 20 | build-test: 21 | name: "Build, test, publish" 22 | uses: ./.github/workflows/ci_common.yml 23 | with: 24 | prNumber: ${{ github.event.pull_request.number }} 25 | release: false 26 | runFullTests: false 27 | secrets: 28 | AZDO_TOKEN: ${{ secrets.AZDO_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/untagged-image-cleanup.yml: -------------------------------------------------------------------------------- 1 | name: untagged_image_cleanup 2 | 3 | on: # yamllint disable-line rule:truthy 4 | schedule: 5 | - cron: "4 18 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | 10 | clean_images: 11 | name: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | persist-credentials: false 18 | 19 | - name: Clean untagged GitHub container images 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | run: 23 | ./.github/workflows/clean_untagged.sh 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | node_modules 3 | !cli/dist/**/node_modules 4 | 5 | # Rest pulled from https://github.com/github/gitignore/blob/master/Node.gitignore 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | jspm_packages/ 47 | 48 | # TypeScript v1 declaration files 49 | typings/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional REPL history 61 | .node_repl_history 62 | 63 | # Output of 'npm pack' 64 | *.tgz 65 | 66 | # Yarn Integrity file 67 | .yarn-integrity 68 | 69 | # dotenv environment variables file 70 | .env 71 | .env.test 72 | 73 | # parcel-bundler cache (https://parceljs.org/) 74 | .cache 75 | 76 | # next.js build output 77 | .next 78 | 79 | # nuxt.js build output 80 | .nuxt 81 | 82 | # vuepress build output 83 | .vuepress/dist 84 | 85 | # Serverless directories 86 | .serverless/ 87 | 88 | # FuseBox cache 89 | .fusebox/ 90 | 91 | # DynamoDB Local files 92 | .dynamodb/ 93 | 94 | # OS metadata 95 | .DS_Store 96 | Thumbs.db 97 | 98 | # Ignore built ts files 99 | __tests__/runner/* 100 | lib/**/* 101 | 102 | 103 | *.vsix 104 | 105 | 106 | output 107 | 108 | .taskkey 109 | 110 | common_lib 111 | lib 112 | dist 113 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Jest Debug opened file", 9 | "type": "node", 10 | "request": "launch", 11 | "runtimeExecutable": "npm", 12 | "args": [ 13 | "test", 14 | "--", 15 | // "--testPathPattern", 16 | // "${fileBasenameNoExtension}", 17 | "--runInBand", 18 | "--all", 19 | "--watchAll" 20 | ], 21 | "cwd": "${workspaceRoot}/azdo-task/DevcontainersCi", 22 | "protocol": "inspector", 23 | "console": "integratedTerminal", 24 | "internalConsoleOptions": "neverOpen" 25 | }, 26 | ] 27 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.format.semicolons": "remove", 3 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, 4 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, 5 | "typescript.format.enable": true, 6 | "typescript.format.insertSpaceAfterCommaDelimiter": true, 7 | "typescript.format.insertSpaceAfterConstructor": false, 8 | "typescript.format.semicolons": "insert", 9 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This repo contains a dev container that is intended to be used for developing the project. 4 | 5 | As a general rule, it is recommended that you create an issue to discuss proposed changes/features before investing too much effort implementing it. For small changes/fixes, it may be easier to just create a PR and discuss it there. 6 | 7 | ## Repo Structure 8 | 9 | The repo contains code for both a GitHub action and Azure DevOps Pipeline Task. 10 | The code for each is contained in a separate folder. 11 | 12 | The main folders in the repo are: 13 | 14 | | Folder | Description | 15 | | --------------- | ---------------------------------------------------------------------------------------------- | 16 | | `azdo-task` | Code for the Azure DevOps Pipeline Task | 17 | | `common` | Common code used by both the GitHub Action and Azure DevOps Pipeline Task | 18 | | `docs` | Documentation for using the action/task | 19 | | `github-action` | Code for the GitHub Action | 20 | | `github-tests` | This folder contains various test projects that are used by the CI for GitHub and Azure DevOps | 21 | 22 | 23 | ## Workflow 24 | 25 | The code for both the GitHub Action and Azure DevOps Pipeline Task is written in TypeScript. To ensure that the code compiles locally, run `./scripts/build-local.sh` from the root of the repo. 26 | 27 | The intention is for the GitHub action and Azure DevOps task to maintain feature parity as far as possible. As a general rule, and changes should be implemented in both the GitHub action and the Azure DevOps task. 28 | 29 | Additionally, it is desirable to add new tests to cover any new functionality. 30 | 31 | When a PR is created, some tests will be automatically triggered against the PR. The full suite of tests requires secrets and needs to be triggered by a maintainer. 32 | 33 | Testing and publishing the AzDO task requires the `AZDO_TOKEN` GitHub secret to be an AzDO PAT for monacotools with Build Read & Execute, Marketplace Acquire & Manage, Extensions Read & Manage and Packing Read & Write permissions. If the corresponding account needs JIT access for Project Collection Administrators permissions, enable that and re-evaluate permissions at https://dev.azure.com/monacotools/_usersSettings/permissionsRefresh. 34 | 35 | ## Miscellaneous 36 | 37 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 38 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 39 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 40 | 41 | This project is under an [MIT license](LICENSE.txt). 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) Microsoft Corporation. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dev Container Build and Run (devcontainers/ci) 2 | 3 | The Dev Container Build and Run GitHub Action is aimed at making it easier to re-use [Dev Containers](https://containers.dev) in a GitHub workflow. The Action supports using a Dev Container to run commands for CI, testing, and more, along with pre-building a Dev Container image. Dev Container image building supports [Dev Container Features](https://containers.dev/implementors/features/#devcontainer-json-properties) and automatically places Dev Container [metadata on an image](https://containers.dev/implementors/spec/#image-metadata) label for simplified use. 4 | 5 | > **NOTE:** The Action is not currently capable of taking advantage of pre-built Codespaces. However, pre-built images are supported. 6 | 7 | A similar [Azure DevOps Task](./docs/azure-devops-task.md) is also available! 8 | 9 | Note that this project builds on top of [@devcontainers/cli](https://www.npmjs.com/package/@devcontainers/cli) which can be used in other automation systems. 10 | 11 | ## Quick start 12 | Here are three examples of using the Action for common scenarios. See the [documentation](./docs/github-action.md) for more details and a list of available [inputs](./docs/github-action.md#inputs). 13 | 14 | **Pre-building an image:** 15 | 16 | ```yaml 17 | - name: Pre-build dev container image 18 | uses: devcontainers/ci@v0.3 19 | with: 20 | imageName: ghcr.io/example/example-devcontainer 21 | cacheFrom: ghcr.io/example/example-devcontainer 22 | push: always 23 | ``` 24 | 25 | **Using a Dev Container for a CI build:** 26 | 27 | ```yaml 28 | - name: Run make ci-build in dev container 29 | uses: devcontainers/ci@v0.3 30 | with: 31 | # [Optional] If you have a separate workflow like the one above 32 | # to pre-build your container image, you can reference it here 33 | # to speed up your application build workflows as well! 34 | cacheFrom: ghcr.io/example/example-devcontainer 35 | 36 | push: never 37 | runCmd: make ci-build 38 | ``` 39 | 40 | **Both at once:** 41 | 42 | ```yaml 43 | - name: Pre-build image and run make ci-build in dev container 44 | uses: devcontainers/ci@v0.3 45 | with: 46 | imageName: ghcr.io/example/example-devcontainer 47 | cacheFrom: ghcr.io/example/example-devcontainer 48 | push: always 49 | runCmd: make ci-build 50 | ``` 51 | 52 | ## CHANGELOG 53 | 54 | ### Version 0.3.0 (24th February 2023) 55 | 56 | This version updates the release mechanism for the GitHub action so that only the compiled JavaScript is included in the release. 57 | The primary motivation is to simplify the process for contributing to the action, but a side-benefit should be a reduced download size when using the action. 58 | 59 | ### Version 0.2.0 60 | 61 | This version updates the implementation to use [@devcontainers/cli](https://www.npmjs.com/package/@devcontainers/cli). 62 | 63 | This brings many benefits around compatibility with Dev Containers. One key area is that [Dev Container Features](https://containers.dev/features) can now be used in CI along with recent enhancements like [image label support](https://containers.dev/implementors/reference/#labels). 64 | 65 | ### Version 0.1.x 66 | 67 | 0.1.x versions were the initial version of the action/task and attempted to mimic the behaviour of Dev Containers with manual `docker` commands 68 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Dev Container Build and Run Action' 2 | description: 'Action to simplify using Dev Containers (https://containers.dev) in GitHub workflows' 3 | author: 'devcontainers' 4 | branding: 5 | color: purple 6 | icon: box 7 | inputs: 8 | imageName: 9 | required: false 10 | description: Image name (including registry) 11 | imageTag: 12 | required: false 13 | description: One or more comma-separated image tags (defaults to latest) 14 | platform: 15 | require: false 16 | description: Platforms for which the image should be built. If omitted, defaults to the platform of the GitHub Actions Runner. Multiple platforms should be comma separated. 17 | runCmd: 18 | required: false 19 | description: Specify the command to run after building the dev container image. Can be omitted to skip starting the container. 20 | subFolder: 21 | required: false 22 | description: Specify a child folder (containing a .devcontainer) instead of using the repository root 23 | default: 24 | configFile: 25 | required: false 26 | description: Specify the path to a devcontainer.json file instead of using `./.devcontainer/devcontainer.json` or `./.devcontainer.json` 27 | default: 28 | checkoutPath: 29 | required: false 30 | description: Specify path to checked out folder if not using default (or for testing with nektos/act) 31 | default: . 32 | push: 33 | required: false 34 | default: filter 35 | description: Control when images are pushed. Options are never, filter, always. For filter (default), images are pushed if the refFilterForPush and eventFilterForPush conditions are met 36 | refFilterForPush: 37 | required: false 38 | default: 39 | description: Set the source branches (e.g. refs/heads/main) that are allowed to trigger a push of the dev container image. Leave empty to allow all. 40 | eventFilterForPush: 41 | required: false 42 | default: push 43 | description: Set the event names (e.g. pull_request, push) that are allowed to trigger a push of the dev container image. Defaults to push. Leave empty for all 44 | env: 45 | required: false 46 | description: Specify environment variables to pass to the docker run command 47 | inheritEnv: 48 | required: false 49 | default: false 50 | description: Inherit all environment variables of the runner CI machine. 51 | skipContainerUserIdUpdate: 52 | required: false 53 | default: false 54 | description: For non-root Dev Containers (i.e. where `remoteUser` is specified), the action attempts to make the container user UID and GID match those of the host user. Set this to true to skip this step (defaults to false) 55 | userDataFolder: 56 | required: false 57 | description: Some steps for building the dev container (e.g. container-features) require generating additional build files. To aid docker build cache re-use, you can use this property to set the path that these files will be generated under and use the actions/cache action to cache that folder across runs 58 | cacheFrom: 59 | required: false 60 | description: Specify additional images to use for build caching 61 | noCache: 62 | type: boolean 63 | required: false 64 | default: false 65 | description: Builds the image with `--no-cache` (takes precedence over `cacheFrom`) 66 | cacheTo: 67 | required: false 68 | description: Specify the image to cache the built image to 69 | outputs: 70 | runCmdOutput: 71 | description: The output of the command specified in the runCmd input 72 | runs: 73 | using: 'node20' 74 | main: 'github-action/run-main.js' 75 | post: 'github-action/run-post.js' 76 | post-if: 'success()' 77 | -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | jest.config.js 5 | -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest", "@typescript-eslint"], 3 | "extends": ["plugin:github/recommended"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "rules": { 11 | "eslint-comments/no-use": "off", 12 | "import/no-namespace": "off", 13 | "i18n-text/no-en": "off", 14 | "no-unused-vars": "off", 15 | "@typescript-eslint/no-unused-vars": "error", 16 | "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], 17 | "@typescript-eslint/no-require-imports": "error", 18 | "@typescript-eslint/array-type": "error", 19 | "@typescript-eslint/await-thenable": "error", 20 | "@typescript-eslint/ban-ts-comment": "error", 21 | "camelcase": "off", 22 | "@typescript-eslint/consistent-type-assertions": "error", 23 | "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], 24 | "func-call-spacing": ["error", "never"], 25 | "@typescript-eslint/no-array-constructor": "error", 26 | "@typescript-eslint/no-empty-interface": "error", 27 | "@typescript-eslint/no-explicit-any": "off", 28 | "@typescript-eslint/no-extraneous-class": "error", 29 | "@typescript-eslint/no-for-in-array": "error", 30 | "@typescript-eslint/no-inferrable-types": "error", 31 | "@typescript-eslint/no-misused-new": "error", 32 | "@typescript-eslint/no-namespace": "error", 33 | "@typescript-eslint/no-non-null-assertion": "warn", 34 | "@typescript-eslint/no-unnecessary-qualifier": "error", 35 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 36 | "@typescript-eslint/no-useless-constructor": "error", 37 | "@typescript-eslint/no-var-requires": "error", 38 | "@typescript-eslint/prefer-for-of": "warn", 39 | "@typescript-eslint/prefer-function-type": "warn", 40 | "@typescript-eslint/prefer-includes": "error", 41 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 42 | "@typescript-eslint/promise-function-async": "error", 43 | "@typescript-eslint/require-array-sort-compare": "error", 44 | "@typescript-eslint/restrict-plus-operands": "error", 45 | "semi": ["error", "always"], 46 | "@typescript-eslint/unbound-method": "error", 47 | "no-console": "off" 48 | }, 49 | "env": { 50 | "node": true, 51 | "es6": true, 52 | "jest/globals": true 53 | } 54 | } -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": true, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/Makefile: -------------------------------------------------------------------------------- 1 | help: ## show this help 2 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ 3 | | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%s\033[0m|%s\n", $$1, $$2}' \ 4 | | column -t -s '|' 5 | 6 | npm-install: 7 | npm install 8 | 9 | lint: 10 | npm run lint 11 | 12 | lint-fix: 13 | npm run lint-fix 14 | 15 | build-package: 16 | npm run build && npm run package 17 | 18 | test: build-package 19 | npm test 20 | 21 | ci-package: 22 | npm run all -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/__tests__/exec.test.ts: -------------------------------------------------------------------------------- 1 | import {exec} from '../src/exec'; 2 | 3 | describe('exec', () => { 4 | test('non-silent returns correct output', async () => { 5 | const result = await exec('bash', ['-c', 'echo hi'], {silent: false}); 6 | console.log(result); 7 | expect(result.exitCode).toBe(0); 8 | expect(result.stdout).toStrictEqual('hi\n\n'); 9 | }); 10 | test('silent returns correct output', async () => { 11 | const result = await exec('bash', ['-c', 'echo hi'], {silent: true}); 12 | console.log(result); 13 | expect(result.exitCode).toBe(0); 14 | expect(result.stdout).toStrictEqual('hi\n\n'); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ci", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Azure DevOps task for building and running Dev Containers (https://containers.dev)", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "build": "tsc --build", 9 | "format": "prettier --write **/*.ts", 10 | "format-check": "prettier --check **/*.ts", 11 | "lint": "eslint src/**/*.ts", 12 | "lint-fix": "eslint --fix src/**/*.ts", 13 | "package": "ncc build --source-map --license licenses.txt", 14 | "test": "jest", 15 | "all": "npm run build && npm run format && npm run lint && npm run package && npm test" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/devcontainers/ci.git" 20 | }, 21 | "keywords": [ 22 | "actions", 23 | "node", 24 | "setup" 25 | ], 26 | "author": "", 27 | "license": "MIT", 28 | "dependencies": { 29 | "jsonc-parser": "^3.3.1" 30 | }, 31 | "devDependencies": { 32 | "@types/jest": "^29.5.14", 33 | "@types/node": "^22.14.1", 34 | "@types/q": "^1.5.8", 35 | "@typescript-eslint/eslint-plugin": "^8.31.0", 36 | "@typescript-eslint/parser": "^8.31.0", 37 | "@vercel/ncc": "^0.38.3", 38 | "azure-pipelines-task-lib": "^5.2.0", 39 | "eslint": "^8.57.0", 40 | "eslint-plugin-github": "^5.1.8", 41 | "eslint-plugin-jest": "^28.11.0", 42 | "jest": "^29.7.0", 43 | "jest-circus": "^29.7.0", 44 | "js-yaml": "^4.1.0", 45 | "prettier": "^3.5.3", 46 | "ts-jest": "^29.3.2", 47 | "typescript": "^5.8.3" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/run-main.js: -------------------------------------------------------------------------------- 1 | const { runMain } = require("./dist/index.js"); 2 | 3 | runMain(); -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/run-post.js: -------------------------------------------------------------------------------- 1 | const { runPost } = require("./dist/index.js"); 2 | 3 | runPost(); -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/src/docker.ts: -------------------------------------------------------------------------------- 1 | import * as task from 'azure-pipelines-task-lib/task'; 2 | import * as docker from '../../../common/src/docker'; 3 | import {exec} from './exec'; 4 | 5 | export async function isDockerBuildXInstalled(): Promise { 6 | return await docker.isDockerBuildXInstalled(exec); 7 | } 8 | export async function buildImage( 9 | imageName: string, 10 | imageTag: string | undefined, 11 | checkoutPath: string, 12 | subFolder: string, 13 | skipContainerUserIdUpdate: boolean, 14 | cacheFrom: string[], 15 | cacheTo: string[], 16 | ): Promise { 17 | console.log('🏗 Building dev container...'); 18 | try { 19 | return await docker.buildImage( 20 | exec, 21 | imageName, 22 | imageTag, 23 | checkoutPath, 24 | subFolder, 25 | skipContainerUserIdUpdate, 26 | cacheFrom, 27 | cacheTo, 28 | ); 29 | } catch (error) { 30 | task.setResult(task.TaskResult.Failed, error); 31 | return ''; 32 | } 33 | } 34 | 35 | export async function runContainer( 36 | imageName: string, 37 | imageTag: string | undefined, 38 | checkoutPath: string, 39 | subFolder: string, 40 | command: string, 41 | envs?: string[], 42 | ): Promise { 43 | console.log('🏃‍♀️ Running dev container...'); 44 | try { 45 | await docker.runContainer( 46 | exec, 47 | imageName, 48 | imageTag, 49 | checkoutPath, 50 | subFolder, 51 | command, 52 | envs, 53 | ); 54 | return true; 55 | } catch (error) { 56 | task.setResult(task.TaskResult.Failed, error); 57 | return false; 58 | } 59 | } 60 | 61 | export async function pushImage( 62 | imageName: string, 63 | imageTag: string | undefined, 64 | ): Promise { 65 | console.log('📌 Pushing image...'); 66 | try { 67 | await docker.pushImage(exec, imageName, imageTag); 68 | return true; 69 | } catch (error) { 70 | task.setResult(task.TaskResult.Failed, error); 71 | return false; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/src/exec.ts: -------------------------------------------------------------------------------- 1 | import * as task from 'azure-pipelines-task-lib/task'; 2 | import {ExecOptions, ExecResult} from '../../../common/src/exec'; 3 | import * as stream from 'stream'; 4 | 5 | // https://github.com/microsoft/azure-pipelines-task-lib/blob/master/node/docs/azure-pipelines-task-lib.md 6 | 7 | /* global BufferEncoding */ 8 | 9 | class TeeStream extends stream.Writable { 10 | private value = ''; 11 | private teeStream: stream.Writable; 12 | 13 | constructor(teeStream: stream.Writable, options?: stream.WritableOptions) { 14 | super(options); 15 | this.teeStream = teeStream; 16 | } 17 | 18 | _write(data: any, encoding: BufferEncoding, callback: Function): void { 19 | this.value += data; 20 | this.teeStream.write(data, encoding); // NOTE - currently ignoring teeStream callback 21 | 22 | if (callback) { 23 | callback(); 24 | } 25 | } 26 | 27 | toString(): string { 28 | return this.value; 29 | } 30 | } 31 | class NullStream extends stream.Writable { 32 | _write(data: any, encoding: BufferEncoding, callback: Function): void { 33 | if (callback) { 34 | callback(); 35 | } 36 | } 37 | } 38 | function trimCommand(input: string): string { 39 | if (input.startsWith('[command]')) { 40 | const newLine = input.indexOf('\n'); 41 | return input.substring(newLine + 1); 42 | } 43 | return input; 44 | } 45 | export async function exec( 46 | command: string, 47 | args: string[], 48 | options: ExecOptions, 49 | ): Promise { 50 | const outStream = new TeeStream( 51 | options.silent ? new NullStream() : process.stdout, 52 | ); 53 | const errStream = new TeeStream( 54 | options.silent ? new NullStream() : process.stderr, 55 | ); 56 | 57 | const exitCode = await task.exec(command, args, { 58 | failOnStdErr: false, 59 | silent: false, // always run non-silent - we just don't output to process.stdout/stderr with the TeeStreams above 60 | ignoreReturnCode: true, 61 | outStream, 62 | errStream, 63 | }); 64 | 65 | return { 66 | exitCode, 67 | stdout: trimCommand(outStream.toString()), 68 | stderr: errStream.toString(), 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as task from 'azure-pipelines-task-lib/task'; 2 | import {TaskResult} from 'azure-pipelines-task-lib/task'; 3 | import path from 'path'; 4 | import {populateDefaults} from '../../../common/src/envvars'; 5 | import { 6 | devcontainer, 7 | DevContainerCliBuildArgs, 8 | DevContainerCliExecArgs, 9 | DevContainerCliUpArgs, 10 | } from '../../../common/src/dev-container-cli'; 11 | 12 | import {isDockerBuildXInstalled, pushImage} from './docker'; 13 | import {isSkopeoInstalled, copyImage} from './skopeo'; 14 | import {exec} from './exec'; 15 | 16 | export async function runMain(): Promise { 17 | try { 18 | task.setTaskVariable('hasRunMain', 'true'); 19 | const buildXInstalled = await isDockerBuildXInstalled(); 20 | if (!buildXInstalled) { 21 | console.log( 22 | '### WARNING: docker buildx not available: add a step to set up with docker/setup-buildx-action - see https://github.com/devcontainers/ci/blob/main/docs/azure-devops-task.md', 23 | ); 24 | return; 25 | } 26 | const devContainerCliInstalled = await devcontainer.isCliInstalled(exec); 27 | if (!devContainerCliInstalled) { 28 | console.log('Installing @devcontainers/cli...'); 29 | const success = await devcontainer.installCli(exec); 30 | if (!success) { 31 | task.setResult( 32 | task.TaskResult.Failed, 33 | '@devcontainers/cli install failed!', 34 | ); 35 | return; 36 | } 37 | } 38 | 39 | const checkoutPath = task.getInput('checkoutPath') ?? ''; 40 | const imageName = task.getInput('imageName'); 41 | const imageTag = task.getInput('imageTag'); 42 | const platform = task.getInput('platform'); 43 | const subFolder = task.getInput('subFolder') ?? '.'; 44 | const relativeConfigFile = task.getInput('configFile'); 45 | const runCommand = task.getInput('runCmd'); 46 | const envs = task.getInput('env')?.split('\n') ?? []; 47 | const inheritEnv = (task.getInput('inheritEnv') ?? 'false') === 'true'; 48 | const inputEnvsWithDefaults = populateDefaults(envs, inheritEnv); 49 | const cacheFrom = task.getInput('cacheFrom')?.split('\n') ?? []; 50 | const noCache = (task.getInput('noCache') ?? 'false') === 'true'; 51 | const cacheTo = task.getInput('cacheTo')?.split('\n') ?? []; 52 | const skipContainerUserIdUpdate = 53 | (task.getInput('skipContainerUserIdUpdate') ?? 'false') === 'true'; 54 | 55 | if (platform) { 56 | const skopeoInstalled = await isSkopeoInstalled(); 57 | if (!skopeoInstalled) { 58 | console.log( 59 | 'skopeo not available and is required for multi-platform builds - make sure it is installed on your build agent', 60 | ); 61 | return; 62 | } 63 | } 64 | const buildxOutput = platform ? 'type=oci,dest=/tmp/output.tar' : undefined; 65 | 66 | const log = (message: string): void => console.log(message); 67 | const workspaceFolder = path.resolve(checkoutPath, subFolder); 68 | const configFile = 69 | relativeConfigFile && path.resolve(checkoutPath, relativeConfigFile); 70 | 71 | const resolvedImageTag = imageTag ?? 'latest'; 72 | const imageTagArray = resolvedImageTag.split(/\s*,\s*/); 73 | const fullImageNameArray: string[] = []; 74 | for (const tag of imageTagArray) { 75 | fullImageNameArray.push(`${imageName}:${tag}`); 76 | } 77 | if (imageName) { 78 | if (fullImageNameArray.length === 1) { 79 | if (!noCache && !cacheFrom.includes(fullImageNameArray[0])) { 80 | // If the cacheFrom options don't include the fullImageName, add it here 81 | // This ensures that when building a PR where the image specified in the action 82 | // isn't included in devcontainer.json (or docker-compose.yml), the action still 83 | // resolves a previous image for the tag as a layer cache (if pushed to a registry) 84 | cacheFrom.splice(0, 0, fullImageNameArray[0]); 85 | } 86 | } else { 87 | // Don't automatically add --cache-from if multiple image tags are specified 88 | console.log( 89 | 'Not adding --cache-from automatically since multiple image tags were supplied', 90 | ); 91 | } 92 | } else { 93 | console.log( 94 | '!! imageTag specified without specifying imageName - ignoring imageTag', 95 | ); 96 | } 97 | const buildArgs: DevContainerCliBuildArgs = { 98 | workspaceFolder, 99 | configFile, 100 | imageName: fullImageNameArray, 101 | platform, 102 | additionalCacheFroms: cacheFrom, 103 | output: buildxOutput, 104 | noCache, 105 | cacheTo, 106 | }; 107 | 108 | console.log('\n\n'); 109 | console.log('***'); 110 | console.log('*** Building the dev container'); 111 | console.log('***'); 112 | const buildResult = await devcontainer.build(buildArgs, log); 113 | if (buildResult.outcome !== 'success') { 114 | console.log( 115 | `### ERROR: Dev container build failed: ${buildResult.message} (exit code: ${buildResult.code})\n${buildResult.description}`, 116 | ); 117 | task.setResult(TaskResult.Failed, buildResult.message); 118 | } 119 | if (buildResult.outcome !== 'success') { 120 | return; 121 | } 122 | 123 | if (runCommand) { 124 | console.log('\n\n'); 125 | console.log('***'); 126 | console.log('*** Starting the dev container'); 127 | console.log('***'); 128 | const upArgs: DevContainerCliUpArgs = { 129 | workspaceFolder, 130 | configFile, 131 | additionalCacheFroms: cacheFrom, 132 | skipContainerUserIdUpdate, 133 | env: inputEnvsWithDefaults, 134 | }; 135 | const upResult = await devcontainer.up(upArgs, log); 136 | if (upResult.outcome !== 'success') { 137 | console.log( 138 | `### ERROR: Dev container up failed: ${upResult.message} (exit code: ${upResult.code})\n${upResult.description}`, 139 | ); 140 | task.setResult(TaskResult.Failed, upResult.message); 141 | } 142 | if (upResult.outcome !== 'success') { 143 | return; 144 | } 145 | 146 | console.log('\n\n'); 147 | console.log('***'); 148 | console.log('*** Running command in the dev container'); 149 | console.log('***'); 150 | const execArgs: DevContainerCliExecArgs = { 151 | workspaceFolder, 152 | configFile, 153 | command: ['bash', '-c', runCommand], 154 | env: inputEnvsWithDefaults, 155 | }; 156 | let execLogString = ''; 157 | const execLog = (message: string): void => { 158 | console.log(message); 159 | if (!message.includes('@devcontainers/cli')) { 160 | execLogString += message; 161 | } 162 | }; 163 | const execResult = await devcontainer.exec(execArgs, execLog); 164 | if (execResult !== 0) { 165 | console.log( 166 | `### ERROR: Dev container exec failed (exit code: ${execResult})`, 167 | ); 168 | task.setResult( 169 | TaskResult.Failed, 170 | `Dev container exec failed (exit code: ${execResult})`, 171 | ); 172 | return; 173 | } 174 | if (execLogString.length >= 25000) { 175 | execLogString = execLogString.substring(0, 24963); 176 | execLogString += 'TRUNCATED TO 25K CHAR MAX OUTPUT SIZE'; 177 | } 178 | console.log(`##vso[task.setvariable variable=runCmdOutput]${execLog}`); 179 | } else { 180 | console.log('No runCmd set - skipping starting/running container'); 181 | } 182 | 183 | // TODO - should we stop the container? 184 | } catch (err) { 185 | task.setResult(task.TaskResult.Failed, err.message); 186 | } 187 | } 188 | 189 | export async function runPost(): Promise { 190 | const pushOption = task.getInput('push'); 191 | const imageName = task.getInput('imageName'); 192 | const pushOnFailedBuild = 193 | (task.getInput('pushOnFailedBuild') ?? 'false') === 'true'; 194 | 195 | // default to 'never' if not set and no imageName 196 | if (pushOption === 'never' || (!pushOption && !imageName)) { 197 | console.log(`Image push skipped because 'push' is set to '${pushOption}'`); 198 | return; 199 | } 200 | 201 | // default to 'filter' if not set and imageName is set 202 | if (pushOption === 'filter' || (!pushOption && imageName)) { 203 | // https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&tabs=yaml 204 | const agentJobStatus = process.env.AGENT_JOBSTATUS; 205 | switch (agentJobStatus) { 206 | case 'Succeeded': 207 | case 'SucceededWithIssues': 208 | // continue 209 | break; 210 | 211 | default: 212 | if (!pushOnFailedBuild) { 213 | console.log( 214 | `Image push skipped because Agent JobStatus is '${agentJobStatus}'`, 215 | ); 216 | return; 217 | } 218 | } 219 | 220 | const buildReasonsForPush: string[] = 221 | task.getInput('buildReasonsForPush')?.split('\n') ?? []; 222 | const sourceBranchFilterForPush: string[] = 223 | task.getInput('sourceBranchFilterForPush')?.split('\n') ?? []; 224 | 225 | // check build reason is allowed 226 | const buildReason = process.env.BUILD_REASON; 227 | if ( 228 | buildReasonsForPush.length !== 0 && // empty filter allows all 229 | !buildReasonsForPush.some(s => s === buildReason) 230 | ) { 231 | console.log( 232 | `Image push skipped because buildReason (${buildReason}) is not in buildReasonsForPush`, 233 | ); 234 | return; 235 | } 236 | 237 | // check branch is allowed 238 | const sourceBranch = process.env.BUILD_SOURCEBRANCH; 239 | if ( 240 | sourceBranchFilterForPush.length !== 0 && // empty filter allows all 241 | !sourceBranchFilterForPush.some(s => s === sourceBranch) 242 | ) { 243 | console.log( 244 | `Image push skipped because source branch (${sourceBranch}) is not in sourceBranchFilterForPush`, 245 | ); 246 | return; 247 | } 248 | } 249 | 250 | if (!imageName) { 251 | if (pushOption) { 252 | // pushOption was set (and not to "never") - give an error that imageName is required 253 | task.setResult( 254 | task.TaskResult.Failed, 255 | `imageName input is required to push images (push: ${pushOption})`, 256 | ); 257 | } 258 | return; 259 | } 260 | const imageTag = task.getInput('imageTag') ?? 'latest'; 261 | const imageTagArray = imageTag.split(/\s*,\s*/); 262 | const platform = task.getInput('platform'); 263 | if (platform) { 264 | for (const tag of imageTagArray) { 265 | console.log(`Copying multiplatform image '${imageName}:${tag}'...`); 266 | const imageSource = `oci-archive:/tmp/output.tar:${tag}`; 267 | const imageDest = `docker://${imageName}:${tag}`; 268 | 269 | await copyImage(true, imageSource, imageDest); 270 | } 271 | } else { 272 | for (const tag of imageTagArray) { 273 | console.log(`Pushing image '${imageName}:${tag}'...`); 274 | await pushImage(imageName, tag); 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/src/skopeo.ts: -------------------------------------------------------------------------------- 1 | import * as task from 'azure-pipelines-task-lib/task'; 2 | import * as skopeo from '../../../common/src/skopeo'; 3 | import {exec} from './exec'; 4 | 5 | export async function isSkopeoInstalled(): Promise { 6 | return await skopeo.isSkopeoInstalled(exec); 7 | } 8 | 9 | export async function copyImage( 10 | all: boolean, 11 | source: string, 12 | dest: string, 13 | ): Promise { 14 | console.log('📌 Copying image...'); 15 | try { 16 | await skopeo.copyImage(exec, all, source, dest); 17 | return true; 18 | } catch (error) { 19 | task.setResult(task.TaskResult.Failed, error); 20 | return false; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/task.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/microsoft/azure-pipelines-task-lib/master/tasks.schema.json", 3 | "id": "d784888b-f54b-4926-a8d1-3a159d2de8e0", 4 | "name": "DevcontainersCi", 5 | "friendlyName": "Devcontainers CI Task", 6 | "description": "Build and run Dev Containers (https://containers.dev) in Azure DevOps Pipelines", 7 | "author": "Devcontainers", 8 | "helpMarkDown": "", 9 | "category": "Build", 10 | "version": { 11 | "Major": 0, 12 | "Minor": 3, 13 | "Patch": 0 14 | }, 15 | "visibility": [ 16 | "Build", 17 | "Release" 18 | ], 19 | "demands": [ 20 | "npm" 21 | ], 22 | "minimumAgentVersion": "1.83.0", 23 | "instanceNameFormat": "Build and Run Dev Container: $(rootFolder)", 24 | "inputs": [ 25 | { 26 | "name": "imageName", 27 | "type": "string", 28 | "label": "Image name (including registry)", 29 | "required": false 30 | }, 31 | { 32 | "name": "imageTag", 33 | "type": "string", 34 | "label": "One or more comma-separated image tags (defaults to latest)", 35 | "required": false 36 | }, 37 | { 38 | "name": "platform", 39 | "type": "string", 40 | "label": "Platforms for which the image should be built. If omitted, defaults to the platform of the Azure DevOps Agent. Multiple platforms should be comma separated.", 41 | "required": false 42 | }, 43 | { 44 | "name": "runCmd", 45 | "type": "multiLine", 46 | "label": "Specify the command to run after building the dev container image", 47 | "required": false 48 | }, 49 | { 50 | "name": "subFolder", 51 | "type": "string", 52 | "label": "Specify a child folder (containing a .devcontainer) instead of using the repository root", 53 | "required": false 54 | }, 55 | { 56 | "name": "configFile", 57 | "type": "string", 58 | "label": "Specify the path to a devcontainer.json file instead of using `./.devcontainer/devcontainer.json` or `./.devcontainer.json`", 59 | "required": false 60 | }, 61 | { 62 | "name": "env", 63 | "type": "multiLine", 64 | "label": "Specify environment variables to pass to the docker run command", 65 | "required": false 66 | }, 67 | { 68 | "name": "inheritEnv", 69 | "type": "boolean", 70 | "label": "Inherit all environment variables of the runner CI machine", 71 | "defaultValue": false, 72 | "required": false 73 | }, 74 | { 75 | "name": "push", 76 | "type": "pickList", 77 | "options": { 78 | "never": "Never push", 79 | "filter": "Push if buildReasonsForPush, sourceBranchFilterForPush, and pushOnFailedBuild conditions are met", 80 | "always": "Always push" 81 | }, 82 | "required": false, 83 | "label": "Control when images are pushed to the registry" 84 | }, 85 | { 86 | "name": "pushOnFailedBuild", 87 | "type": "boolean", 88 | "defaultValue": false, 89 | "required": false, 90 | "label": "Control whether to push the image on failed builds (if push==filter)" 91 | }, 92 | { 93 | "name": "buildReasonsForPush", 94 | "type": "multiLine", 95 | "label": "Set the Build Reasons that should trigger a push of the dev container image (if push=filter). Defaults to Manual, IndividualCI, BatchedCI. (see https://docs.microsoft.com/en-us/azure/devops/pipelines/build/variables?view=azure-devops&viewFallbackFrom=vsts&tabs=yaml)", 96 | "required": false, 97 | "defaultValue": "Manual\nIndividualCI\nBatchedCI" 98 | }, 99 | { 100 | "name": "sourceBranchFilterForPush", 101 | "type": "multiLine", 102 | "label": "Set the source branches (e.g. refs/heads/main) that are allowed to trigger a push of the dev container image (if push=filter). Leave empty to allow all.", 103 | "required": false, 104 | "defaultValue": "" 105 | }, 106 | { 107 | "name": "skipContainerUserIdUpdate", 108 | "type": "boolean", 109 | "label": "For non-root Dev Containers (i.e. where `remoteUser` is specified), the action attempts to make the container user UID and GID match those of the host user. Set this to true to skip this step (defaults to false)", 110 | "required": false, 111 | "defaultValue": false 112 | 113 | }, 114 | { 115 | "name": "cacheFrom", 116 | "type": "multiLine", 117 | "label": "Specify additional images to use for build caching", 118 | "required": false 119 | }, 120 | { 121 | "name": "noCache", 122 | "type": "boolean", 123 | "label": "Builds the image with `--no-cache` (takes precedence over `cacheFrom`)", 124 | "required": false 125 | }, 126 | { 127 | "name": "cacheTo", 128 | "type": "multiLine", 129 | "label": "Specify the image to cache the built image to", 130 | "required": false 131 | } 132 | ], 133 | "outputVariables": [{ 134 | "name": "runCmdOutput", 135 | "description": "The output of the command specified in the runCmd input" 136 | }], 137 | "execution": { 138 | "Node16": { 139 | "target": "run-main.js", 140 | "argumentFormat": "" 141 | } 142 | }, 143 | "postjobexecution": { 144 | "Node16": { 145 | "target": "run-post.js", 146 | "argumentFormat": "" 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /azdo-task/DevcontainersCi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib", 5 | "rootDir": "./src" 6 | }, 7 | "references": [ 8 | { 9 | "path": "../../common" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /azdo-task/LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) Microsoft Corporation. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. -------------------------------------------------------------------------------- /azdo-task/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devcontainers/ci/26831aa9da924018a5b4e0dee47d60cbe65ffde9/azdo-task/icon.png -------------------------------------------------------------------------------- /azdo-task/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1 3 | } 4 | -------------------------------------------------------------------------------- /azdo-task/scripts/build-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | 5 | script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 6 | 7 | 8 | function show_usage() { 9 | echo 10 | echo "build-package.sh" 11 | echo 12 | echo "Build the AzDO task package" 13 | echo 14 | echo -e "\t--set-patch-version\t(Optional)If set then the patch version will be updated before packaging" 15 | echo 16 | } 17 | # Set default values here 18 | set_patch_version="" 19 | 20 | # Process switches: 21 | while [[ $# -gt 0 ]] 22 | do 23 | case "$1" in 24 | --set-patch-version) 25 | set_patch_version=$2 26 | if [[ -z $2 ]]; then 27 | shift 1 28 | else 29 | shift 2 30 | fi 31 | ;; 32 | *) 33 | echo "Unexpected '$1'" 34 | show_usage 35 | exit 1 36 | ;; 37 | esac 38 | done 39 | 40 | figlet Version 41 | 42 | if [[ -n $set_patch_version ]]; then 43 | echo "--set-patch-version specified. Setting task patch version to $set_patch_version" 44 | sed -i "s/\"Patch\": 0/\"Patch\": $set_patch_version/g" DevcontainersCi/task.json 45 | else 46 | echo "--set-patch-version not set" 47 | fi 48 | 49 | VERSION_MAJOR=$(cat DevcontainersCi/task.json |jq .version.Major) 50 | VERSION_MINOR=$(cat DevcontainersCi/task.json |jq .version.Minor) 51 | VERSION_PATCH=$(cat DevcontainersCi/task.json |jq .version.Patch) 52 | 53 | echo "VERSION_MAJOR=${VERSION_MAJOR}" 54 | echo "VERSION_MINOR=${VERSION_MINOR}" 55 | echo "VERSION_PATCH=${VERSION_PATCH}" 56 | 57 | echo "version=$VERSION_MAJOR.$VERSION_MINOR.$VERSION_PATCH" >> "$GITHUB_OUTPUT" 58 | echo "version_short=$VERSION_MAJOR.$VERSION_MINOR" >> "$GITHUB_OUTPUT" 59 | echo "version_major=$VERSION_MAJOR" >> "$GITHUB_OUTPUT" 60 | 61 | if [[ -n $set_patch_version ]]; then 62 | echo "--set-patch-version specified. Setting extension version to $VERSION_MAJOR.$VERSION_MINOR.$VERSION_PATCH" 63 | sed -i "s/\"version\": \"[0-9.]*\"/\"version\": \"$VERSION_MAJOR.$VERSION_MINOR.$VERSION_PATCH\"/g" vss-extension.json 64 | fi 65 | 66 | 67 | figlet Build task 68 | cd "$script_dir/../DevcontainersCi" 69 | npm install 70 | npm run all 71 | 72 | echo prune 73 | npm prune --production 74 | 75 | figlet Package extension 76 | cd "$script_dir/../" 77 | echo "Build devcontainers extension vsix..." 78 | tfx extension create --manifests vss-extension.json --override "{\"public\": true, \"version\": \"$VERSION_MAJOR.$VERSION_MINOR.$VERSION_PATCH\"}" 79 | echo "Build devcontainers-dev extension vsix..." 80 | tfx extension create --manifests vss-extension.json --override "{\"public\": false, \"version\": \"$VERSION_MAJOR.$VERSION_MINOR.$VERSION_PATCH\", \"publisher\": \"devcontainers-dev\"}" 81 | -------------------------------------------------------------------------------- /azdo-task/scripts/run-azdo-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | function show_usage() { 5 | echo 6 | echo "run-azdo-build.sh" 7 | echo 8 | echo "Run an AzDO build. Assumes that auth has been configured, e.g. via the AZURE_DEVOPS_EXT_PAT env var (https://docs.microsoft.com/en-gb/azure/devops/cli/log-in-via-pat?view=azure-devops&tabs=windows)" 9 | echo 10 | echo -e "\t--organization\t(Required)The AzDO organization url" 11 | echo -e "\t--project\t(Required)The AzDO project name" 12 | echo -e "\t--build\t(Required)The build name" 13 | echo -e "\t--image-tag\t(Required)The image tag for dev containers build in the pipeline" 14 | echo -e "\t--commit\t(Required)The commit id to build" 15 | echo 16 | } 17 | 18 | 19 | # Set default values here 20 | organization="" 21 | project="" 22 | build="" 23 | image_tag="" 24 | commit_id="" 25 | 26 | # Process switches: 27 | while [[ $# -gt 0 ]] 28 | do 29 | case "$1" in 30 | --organization) 31 | organization=$2 32 | shift 2 33 | ;; 34 | --project) 35 | project=$2 36 | shift 2 37 | ;; 38 | --build) 39 | build=$2 40 | shift 2 41 | ;; 42 | --image-tag) 43 | image_tag=$2 44 | shift 2 45 | ;; 46 | --commit) 47 | commit_id=$2 48 | shift 2 49 | ;; 50 | *) 51 | echo "Unexpected '$1'" 52 | show_usage 53 | exit 1 54 | ;; 55 | esac 56 | done 57 | 58 | 59 | if [[ -z $organization ]]; then 60 | echo "--organization must be specified" 61 | show_usage 62 | exit 1 63 | fi 64 | if [[ -z $project ]]; then 65 | echo "--project must be specified" 66 | show_usage 67 | exit 1 68 | fi 69 | if [[ -z $build ]]; then 70 | echo "--build must be specified" 71 | show_usage 72 | exit 1 73 | fi 74 | if [[ -z $image_tag ]]; then 75 | echo "--image-tag must be specified" 76 | show_usage 77 | exit 1 78 | fi 79 | if [[ -z $commit_id ]]; then 80 | echo "--commit must be specified" 81 | show_usage 82 | exit 1 83 | fi 84 | 85 | 86 | echo "Starting AzDO pipeline..." 87 | run_json=$(az pipelines build queue --definition-name "$build" --organization "$organization" --project "$project" --commit-id "$commit_id" --variables IMAGE_TAG=$image_tag -o json) 88 | run_id=$(echo $run_json | jq -r .id) 89 | run_url="$organization/$project/_build/results?buildId=$run_id" 90 | echo "Run id: $run_id" 91 | echo "Run url: $run_url" 92 | while true 93 | do 94 | run_state=$(az pipelines runs show --id "$run_id" --organization "$organization" --project "$project" -o json) 95 | finish_time=$(echo $run_state | jq -r .finishTime) 96 | if [[ $finish_time != "null" ]]; then 97 | result=$(echo $run_state | jq -r .result) 98 | echo "Pipeline completed with result: $result" 99 | if [[ $result != "succeeded" && $result != "partiallySucceeded" ]]; then 100 | echo "Run url: $run_url" 101 | echo "::error ::AzDO pipeline test did not complete successfully" 102 | exit 1 103 | fi 104 | exit 0 105 | fi 106 | echo "waiting for pipeline completion..." 107 | sleep 15s 108 | done 109 | 110 | 111 | -------------------------------------------------------------------------------- /azdo-task/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "./lib", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "useUnknownInCatchVariables": false, 9 | "noImplicitAny": true, 10 | "esModuleInterop": true 11 | }, 12 | "exclude": ["node_modules", "**/*.test.ts"], 13 | "references": [ 14 | { "path": "../common"} 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /azdo-task/vss-extension.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifestVersion": 1, 3 | "id": "ci", 4 | "name": "Dev Container Build and Run Task", 5 | "version": "0.0.0", 6 | "publisher": "devcontainers", 7 | "targets": [ 8 | { 9 | "id": "Microsoft.VisualStudio.Services" 10 | } 11 | ], 12 | "description": "Extension for building and running Dev Containers (https://containers.dev) in Azure DevOps Pipelines", 13 | "categories": [ 14 | "Azure Pipelines" 15 | ], 16 | "tags": [ 17 | "Visual Studio Extensions", 18 | "Dev Containers", 19 | "Devcontainers" 20 | ], 21 | "links": { 22 | "repository": { 23 | "uri": "https://github.com/devcontainers/ci" 24 | }, 25 | "issues": { 26 | "uri": "https://github.com/devcontainers/ci/issues" 27 | } 28 | }, 29 | "icons": { 30 | "default": "icon.png" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "uri": "https://github.com/devcontainers/ci" 35 | }, 36 | "content": { 37 | "details": { 38 | "path": "README.md" 39 | }, 40 | "license": { 41 | "path": "LICENSE.md" 42 | } 43 | }, 44 | "files": [ 45 | { 46 | "path": "DevcontainersCi" 47 | } 48 | ], 49 | "contributions": [ 50 | { 51 | "id": "DevcontainersCi", 52 | "type": "ms.vss-distributed-task.task", 53 | "targets": [ 54 | "ms.vss-distributed-task.tasks" 55 | ], 56 | "properties": { 57 | "name": "DevcontainersCi" 58 | } 59 | } 60 | ] 61 | } -------------------------------------------------------------------------------- /common/__tests__/config.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DevContainerConfig, 3 | getRemoteUser, 4 | getWorkspaceFolder, 5 | loadFromString, 6 | } from '../src/config'; 7 | 8 | describe('getWorkspaceFolder', () => { 9 | // TODO - need to check workspaceMount/workspaceFolder to set the source mount (https://github.com/devcontainers/ci/issues/66) 10 | // test('returns configured workspaceFolder', () => { 11 | // const repoPath = '/path/to/project-folder' 12 | // const devcontainerConfig: DevContainerConfig = { 13 | // workspaceFolder: '/customMount' 14 | // } 15 | // const result = getWorkspaceFolder(devcontainerConfig, repoPath) 16 | // expect(result).toBe('/customMount') 17 | // }) 18 | test('returns default from repo path when not configured', () => { 19 | const repoPath = '/path/to/project-folder'; 20 | const devcontainerConfig: DevContainerConfig = {}; 21 | const result = getWorkspaceFolder(devcontainerConfig, repoPath); 22 | expect(result).toBe('/workspaces/project-folder'); 23 | }); 24 | }); 25 | 26 | describe('getRemoteUser', () => { 27 | test('returns configured user name', () => { 28 | const devcontainerConfig: DevContainerConfig = { 29 | remoteUser: 'myUser', 30 | }; 31 | const result = getRemoteUser(devcontainerConfig); 32 | expect(result).toBe('myUser'); 33 | }); 34 | test('returns `root` when not configured', () => { 35 | const devcontainerConfig: DevContainerConfig = {}; 36 | const result = getRemoteUser(devcontainerConfig); 37 | expect(result).toBe('root'); 38 | }); 39 | }); 40 | 41 | describe('load', () => { 42 | const json = `{ 43 | "workspaceFolder": "/workspace/path", 44 | "remoteUser": "myUser", 45 | "build" : { 46 | "args" : { 47 | "ARG1": "value1", 48 | "ARG2": "value2", 49 | } 50 | }, 51 | "runArgs" : [ 52 | "test1", 53 | "test2" 54 | ], 55 | "mounts" : [ 56 | "/mnt/test/1", 57 | "/mnt/test/2" 58 | ] 59 | }`; 60 | const devcontainerConfig = loadFromString(json); 61 | 62 | test('workspaceFolder is correct', () => { 63 | expect(devcontainerConfig.workspaceFolder).toBe('/workspace/path'); 64 | }); 65 | test('remoteUser is correct', () => { 66 | expect(devcontainerConfig.remoteUser).toBe('myUser'); 67 | }); 68 | test('build.args to be correct', () => { 69 | if (!devcontainerConfig.build) { 70 | expect(devcontainerConfig.build).toBeDefined(); 71 | return; 72 | } 73 | if (!devcontainerConfig.build.args) { 74 | expect(devcontainerConfig.build.args).toBeDefined(); 75 | return; 76 | } 77 | expect(devcontainerConfig.build.args['ARG1']).toBe('value1'); 78 | expect(devcontainerConfig.build.args['ARG2']).toBe('value2'); 79 | }); 80 | test('runArgs to be correct', () => { 81 | expect(devcontainerConfig.runArgs).toStrictEqual(['test1', 'test2']); 82 | }); 83 | test('mounts to be correct', () => { 84 | expect(devcontainerConfig.mounts).toStrictEqual(['/mnt/test/1', '/mnt/test/2']); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /common/__tests__/docker.test.ts: -------------------------------------------------------------------------------- 1 | import {parseMount} from '../src/docker'; 2 | 3 | describe('parseMount', () => { 4 | test('handles type,src,dst', () => { 5 | const input = 'type=bind,src=/my/source,dst=/my/dest'; 6 | const result = parseMount(input); 7 | expect(result.type).toBe('bind'); 8 | expect(result.source).toBe('/my/source'); 9 | expect(result.target).toBe('/my/dest'); 10 | }); 11 | test('handles type,source,destination', () => { 12 | const input = 'type=bind,source=/my/source,destination=/my/dest'; 13 | const result = parseMount(input); 14 | expect(result.type).toBe('bind'); 15 | expect(result.source).toBe('/my/source'); 16 | expect(result.target).toBe('/my/dest'); 17 | }); 18 | test('handles type,source,target', () => { 19 | const input = 'type=bind,source=/my/source,target=/my/dest'; 20 | const result = parseMount(input); 21 | expect(result.type).toBe('bind'); 22 | expect(result.source).toBe('/my/source'); 23 | expect(result.target).toBe('/my/dest'); 24 | }); 25 | 26 | test('throws on unexpected option', () => { 27 | const input = 'type=bind,source=/my/source,target=/my/dest,made-up'; 28 | const action = () => parseMount(input); 29 | expect(action).toThrow("Unhandled mount option 'made-up'"); 30 | }); 31 | 32 | test('ignores readonly', () => { 33 | const input = 'type=bind,source=/my/source,target=/my/dest,readonly'; 34 | const result = parseMount(input); 35 | expect(result.type).toBe('bind'); 36 | expect(result.source).toBe('/my/source'); 37 | expect(result.target).toBe('/my/dest'); 38 | }); 39 | test('ignores ro', () => { 40 | const input = 'type=bind,source=/my/source,target=/my/dest,ro'; 41 | const result = parseMount(input); 42 | expect(result.type).toBe('bind'); 43 | expect(result.source).toBe('/my/source'); 44 | expect(result.target).toBe('/my/dest'); 45 | }); 46 | test('ignores readonly with value', () => { 47 | const input = 'type=bind,source=/my/source,target=/my/dest,readonly=false'; 48 | const result = parseMount(input); 49 | expect(result.type).toBe('bind'); 50 | expect(result.source).toBe('/my/source'); 51 | expect(result.target).toBe('/my/dest'); 52 | }); 53 | test('ignores ro with value', () => { 54 | const input = 'type=bind,source=/my/source,target=/my/dest,ro=0'; 55 | const result = parseMount(input); 56 | expect(result.type).toBe('bind'); 57 | expect(result.source).toBe('/my/source'); 58 | expect(result.target).toBe('/my/dest'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /common/__tests__/envvars.test.ts: -------------------------------------------------------------------------------- 1 | import {populateDefaults, substituteValues} from '../src/envvars'; 2 | 3 | describe('substituteValues', () => { 4 | test('returns original string with no substitution placeholders', async () => { 5 | const input = 'This is a test'; 6 | const result = await substituteValues(input); 7 | expect(result).toBe(input); 8 | }); 9 | 10 | test("Handles 'env' substitution placeholders", async () => { 11 | process.env.TEST_ENV1 = 'TestEnvValue1'; 12 | process.env.TEST_ENV2 = 'TestEnvValue2'; 13 | const input = 'TEST_ENV1: ${env:TEST_ENV1}, TEST_ENV2: ${env:TEST_ENV2}'; 14 | const result = await substituteValues(input); 15 | expect(result).toBe('TEST_ENV1: TestEnvValue1, TEST_ENV2: TestEnvValue2'); 16 | }); 17 | 18 | test("Handles 'Env' substitution placeholders", async () => { 19 | process.env.TEST_ENV1 = 'TestEnvValue1'; 20 | process.env.TEST_ENV2 = 'TestEnvValue2'; 21 | const input = 'TEST_ENV1: ${Env:TEST_ENV1}, TEST_ENV2: ${Env:TEST_ENV2}'; 22 | const result = await substituteValues(input); 23 | expect(result).toBe('TEST_ENV1: TestEnvValue1, TEST_ENV2: TestEnvValue2'); 24 | }); 25 | 26 | test("ignores substitution placeholders that it doesn't understand", async () => { 27 | const input = 'TEST_ENV: ${nothingToSee:TEST_ENV}'; 28 | const result = await substituteValues(input); 29 | expect(result).toBe(input); 30 | }); 31 | }); 32 | 33 | describe('populateDefaults', () => { 34 | test('returns original inputs when fully specified', () => { 35 | const input = ['TEST_ENV1=value1', 'TEST_ENV2=value2']; 36 | const result = populateDefaults(input, false); 37 | expect(result).toEqual(['TEST_ENV1=value1', 'TEST_ENV2=value2']); 38 | }); 39 | 40 | test('adds process env value when set and input value not provided', () => { 41 | const input = ['TEST_ENV1', 'TEST_ENV2=value2']; 42 | process.env.TEST_ENV1 = 'TestEnvValue1'; 43 | const result = populateDefaults(input, false); 44 | expect(result).toEqual(['TEST_ENV1=TestEnvValue1', 'TEST_ENV2=value2']); 45 | }); 46 | 47 | test('skips value when process env value not set and input value not provided', () => { 48 | const input = ['TEST_ENV1', 'TEST_ENV2=value2']; 49 | delete process.env.TEST_ENV1; 50 | const result = populateDefaults(input, false); 51 | expect(result).toEqual(['TEST_ENV2=value2']); 52 | }); 53 | 54 | test('inherits process env when asked', () => { 55 | const originalEnv = process.env; 56 | try { 57 | process.env = {TEST_ENV1: 'value1'}; 58 | const input = ['TEST_ENV2=value2']; 59 | const result = populateDefaults(input, true); 60 | expect(result).toEqual(['TEST_ENV1=value1', 'TEST_ENV2=value2']); 61 | } 62 | finally { 63 | process.env = originalEnv; 64 | } 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /common/__tests__/users.test.ts: -------------------------------------------------------------------------------- 1 | import {parsePasswd, parseGroup} from '../src/users'; 2 | 3 | describe('parsePasswd', () => { 4 | test('parses lines correctly', async () => { 5 | const input = `root:x:0:0:root:/root:/bin/bash 6 | stuart:x:1000:1001:,,,:/home/stuart:/bin/bash`; 7 | const result = await parsePasswd(input); 8 | expect(result.length).toBe(2); 9 | expect(result[0].name).toBe('root'); 10 | expect(result[0].uid).toBe('0'); 11 | expect(result[0].gid).toBe('0'); 12 | expect(result[1].name).toBe('stuart'); 13 | expect(result[1].uid).toBe('1000'); 14 | expect(result[1].gid).toBe('1001'); 15 | }); 16 | }); 17 | 18 | describe('parseGroup', () => { 19 | test('parses lines correctly', async () => { 20 | const input = `root:x:0: 21 | test:x:123:stuart 22 | test2:x:1000:stuart,emilie`; 23 | const result = await parseGroup(input); 24 | expect(result.length).toBe(3); 25 | expect(result[0].name).toBe('root'); 26 | expect(result[0].gid).toBe('0'); 27 | expect(result[0].users).toStrictEqual([]); 28 | expect(result[1].name).toBe('test'); 29 | expect(result[1].gid).toBe('123'); 30 | expect(result[1].users).toStrictEqual(['stuart']); 31 | expect(result[2].name).toBe('test2'); 32 | expect(result[2].gid).toBe('1000'); 33 | expect(result[2].users).toStrictEqual(['stuart', 'emilie']); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /common/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } -------------------------------------------------------------------------------- /common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devcontainer-build-run-common", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "common ", 6 | "scripts": { 7 | "build": "tsc --build", 8 | "test": "jest", 9 | "tsc_version": "tsc --version" 10 | }, 11 | "dependencies": { 12 | "jsonc-parser": "^3.3.1" 13 | }, 14 | "devDependencies": { 15 | "@types/node": "^22.14.1", 16 | "@types/jest": "^29.5.14", 17 | "typescript": "^5.8.3", 18 | "jest": "^29.7.0", 19 | "jest-circus": "^29.7.0", 20 | "ts-jest": "^29.3.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /common/src/config.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import * as jsoncParser from 'jsonc-parser'; 4 | 5 | const {readFile} = fs.promises; 6 | 7 | export interface DevContainerConfig { 8 | // see https://code.visualstudio.com/docs/remote/devcontainerjson-reference 9 | workspaceFolder?: string; 10 | remoteUser?: string; 11 | dockerFile?: string; 12 | context?: string; 13 | build?: { 14 | dockerfile?: string; 15 | context?: string; 16 | args?: Record; 17 | cacheFrom?: string | string[]; 18 | cacheTo?: string | string[]; 19 | }; 20 | runArgs?: string[]; 21 | mounts?: string[]; 22 | } 23 | 24 | export async function loadFromFile( 25 | filepath: string, 26 | ): Promise { 27 | const jsonContent = await readFile(filepath); 28 | return loadFromString(jsonContent.toString()); 29 | } 30 | 31 | export function loadFromString(content: string): DevContainerConfig { 32 | const config = jsoncParser.parse(content) as DevContainerConfig; 33 | return config; 34 | } 35 | 36 | export function getWorkspaceFolder( 37 | config: DevContainerConfig, 38 | repoPath: string, 39 | ): string { 40 | // TODO - need to check workspaceMount/workspaceFolder to set the source mount (https://github.com/devcontainers/ci/issues/66) 41 | // // https://code.visualstudio.com/docs/remote/containers-advanced#_changing-the-default-source-code-mount 42 | // if (config.workspaceFolder) { 43 | // return config.workspaceFolder 44 | // } 45 | return path.join('/workspaces', path.basename(repoPath)); 46 | } 47 | 48 | export function getRemoteUser(config: DevContainerConfig): string { 49 | // https://code.visualstudio.com/docs/remote/containers-advanced#_specifying-a-user-for-vs-code 50 | return config.remoteUser ?? 'root'; 51 | } 52 | 53 | export function getDockerfile(config: DevContainerConfig): string | undefined { 54 | return config.build?.dockerfile ?? config.dockerFile; 55 | } 56 | 57 | export function getContext(config: DevContainerConfig): string | undefined { 58 | return config.build?.context ?? config.context; 59 | } 60 | -------------------------------------------------------------------------------- /common/src/dev-container-cli.ts: -------------------------------------------------------------------------------- 1 | import {spawn as spawnRaw} from 'child_process'; 2 | import fs, {fstatSync} from 'fs'; 3 | import path from 'path'; 4 | import {env} from 'process'; 5 | import {promisify} from 'util'; 6 | import {ExecFunction} from './exec'; 7 | import {findWindowsExecutable} from './windows'; 8 | 9 | const cliVersion = "0"; // Use 'latest' to get latest CLI version, or pin to specific version e.g. '0.14.1' if required 10 | 11 | export interface DevContainerCliError { 12 | outcome: 'error'; 13 | code: number; 14 | message: string; 15 | description: string; 16 | } 17 | function getSpecCliInfo() { 18 | // // TODO - this is temporary until the CLI is installed via npm 19 | // // TODO - ^ could consider an `npm install` from the folder 20 | // const specCLIPath = path.resolve(__dirname, "..", "cli", "cli.js"); 21 | // return { 22 | // command: `node ${specCLIPath}`, 23 | // }; 24 | return { 25 | command: 'devcontainer', 26 | }; 27 | } 28 | 29 | async function isCliInstalled(exec: ExecFunction): Promise { 30 | try { 31 | const command = await findWindowsExecutable(getSpecCliInfo().command); 32 | const {exitCode} = await exec(command, ['--help'], { 33 | silent: true, 34 | }); 35 | return exitCode === 0; 36 | } catch (error) { 37 | return false; 38 | } 39 | } 40 | const fstat = promisify(fs.stat); 41 | async function installCli(exec: ExecFunction): Promise { 42 | // if we have a local 'cli' folder, then use that as we're testing a private cli build 43 | let cliStat = null; 44 | try { 45 | cliStat = await fstat('./_devcontainer_cli'); 46 | } catch { 47 | } 48 | if (cliStat && cliStat.isDirectory()) { 49 | console.log('** Installing local cli'); 50 | const {exitCode, stdout, stderr} = await exec('bash', ['-c', 'cd _devcontainer_cli && npm install && npm install -g'], {}); 51 | if (exitCode != 0) { 52 | console.log(stdout); 53 | console.error(stderr); 54 | } 55 | return exitCode === 0; 56 | } 57 | console.log('** Installing @devcontainers/cli'); 58 | const {exitCode, stdout, stderr} = await exec('bash', ['-c', `npm install -g @devcontainers/cli@${cliVersion}`], {}); 59 | if (exitCode != 0) { 60 | console.log(stdout); 61 | console.error(stderr); 62 | } 63 | return exitCode === 0; 64 | } 65 | 66 | interface SpawnResult { 67 | code: number | null; 68 | } 69 | 70 | interface SpawnOptions { 71 | log: (data: string) => void; 72 | err: (data: string) => void; 73 | env: NodeJS.ProcessEnv; 74 | } 75 | function spawn( 76 | command: string, 77 | args: string[], 78 | options: SpawnOptions, 79 | ): Promise { 80 | return new Promise((resolve, reject) => { 81 | const proc = spawnRaw(command, args, {env: options.env}); 82 | 83 | proc.stdout.on('data', data => options.log(data.toString())); 84 | proc.stderr.on('data', data => options.err(data.toString())); 85 | 86 | proc.on('error', err => { 87 | reject(err); 88 | }); 89 | proc.on('close', code => { 90 | resolve({ 91 | code: code, 92 | }); 93 | }); 94 | }); 95 | } 96 | 97 | function parseCliOutput(value: string): T | DevContainerCliError { 98 | if (value === '') { 99 | // TODO - revisit this 100 | throw new Error('Unexpected empty output from CLI'); 101 | } 102 | try { 103 | return JSON.parse(value) as T; 104 | } catch (error) { 105 | return { 106 | code: -1, 107 | outcome: 'error' as 'error', 108 | message: 'Failed to parse CLI output', 109 | description: `Failed to parse CLI output as JSON: ${value}\nError: ${error}`, 110 | }; 111 | } 112 | } 113 | 114 | async function runSpecCliJsonCommand(options: { 115 | args: string[]; 116 | log: (data: string) => void; 117 | env?: NodeJS.ProcessEnv; 118 | }) { 119 | // For JSON commands, pass stderr on to logging but capture stdout and parse the JSON response 120 | let stdout = ''; 121 | const spawnOptions: SpawnOptions = { 122 | log: data => (stdout += data), 123 | err: data => options.log(data), 124 | env: options.env ? {...process.env, ...options.env} : process.env, 125 | }; 126 | const command = await findWindowsExecutable(getSpecCliInfo().command); 127 | console.log(`About to run ${command} ${options.args.join(' ')}`); // TODO - take an output arg to allow GH to use core.info 128 | await spawn(command, options.args, spawnOptions); 129 | 130 | return parseCliOutput(stdout); 131 | } 132 | async function runSpecCliNonJsonCommand(options: { 133 | args: string[]; 134 | log: (data: string) => void; 135 | env?: NodeJS.ProcessEnv; 136 | }) { 137 | // For non-JSON commands, pass both stdout and stderr on to logging 138 | const spawnOptions: SpawnOptions = { 139 | log: data => options.log(data), 140 | err: data => options.log(data), 141 | env: options.env ? {...process.env, ...options.env} : process.env, 142 | }; 143 | const command = await findWindowsExecutable(getSpecCliInfo().command); 144 | console.log(`About to run ${command} ${options.args.join(' ')}`); // TODO - take an output arg to allow GH to use core.info 145 | const result = await spawn(command, options.args, spawnOptions); 146 | return result.code 147 | } 148 | 149 | export interface DevContainerCliSuccessResult { 150 | outcome: 'success'; 151 | } 152 | 153 | export interface DevContainerCliBuildResult 154 | extends DevContainerCliSuccessResult {} 155 | export interface DevContainerCliBuildArgs { 156 | workspaceFolder: string; 157 | configFile: string | undefined; 158 | imageName?: string[]; 159 | platform?: string; 160 | additionalCacheFroms?: string[]; 161 | userDataFolder?: string; 162 | output?: string, 163 | noCache?: boolean, 164 | cacheTo?: string[], 165 | } 166 | async function devContainerBuild( 167 | args: DevContainerCliBuildArgs, 168 | log: (data: string) => void, 169 | ): Promise { 170 | const commandArgs: string[] = [ 171 | 'build', 172 | '--workspace-folder', 173 | args.workspaceFolder, 174 | ]; 175 | if (args.configFile) { 176 | commandArgs.push('--config', args.configFile); 177 | } 178 | if (args.imageName) { 179 | args.imageName.forEach(iName => 180 | commandArgs.push('--image-name', iName), 181 | ); 182 | } 183 | if (args.platform) { 184 | commandArgs.push('--platform', args.platform.split(/\s*,\s*/).join(',')); 185 | } 186 | if (args.output) { 187 | commandArgs.push('--output', args.output); 188 | } 189 | if (args.userDataFolder) { 190 | commandArgs.push("--user-data-folder", args.userDataFolder); 191 | } 192 | if (args.noCache) { 193 | commandArgs.push("--no-cache"); 194 | } else if (args.additionalCacheFroms) { 195 | args.additionalCacheFroms.forEach(cacheFrom => 196 | commandArgs.push('--cache-from', cacheFrom), 197 | ); 198 | } 199 | if (args.cacheTo) { 200 | args.cacheTo.forEach(cacheTo => 201 | commandArgs.push('--cache-to', cacheTo), 202 | ); 203 | } 204 | return await runSpecCliJsonCommand({ 205 | args: commandArgs, 206 | log, 207 | env: {DOCKER_BUILDKIT: '1', COMPOSE_DOCKER_CLI_BUILD: '1'}, 208 | }); 209 | } 210 | 211 | export interface DevContainerCliUpResult extends DevContainerCliSuccessResult { 212 | containerId: string; 213 | remoteUser: string; 214 | remoteWorkspaceFolder: string; 215 | } 216 | export interface DevContainerCliUpArgs { 217 | workspaceFolder: string; 218 | configFile: string | undefined; 219 | additionalCacheFroms?: string[]; 220 | cacheTo?: string[]; 221 | skipContainerUserIdUpdate?: boolean; 222 | env?: string[]; 223 | userDataFolder?: string; 224 | additionalMounts?: string[]; 225 | } 226 | async function devContainerUp( 227 | args: DevContainerCliUpArgs, 228 | log: (data: string) => void, 229 | ): Promise { 230 | const remoteEnvArgs = getRemoteEnvArray(args.env); 231 | const commandArgs: string[] = [ 232 | 'up', 233 | '--workspace-folder', 234 | args.workspaceFolder, 235 | ...remoteEnvArgs, 236 | ]; 237 | if (args.configFile) { 238 | commandArgs.push('--config', args.configFile); 239 | } 240 | if (args.additionalCacheFroms) { 241 | args.additionalCacheFroms.forEach(cacheFrom => 242 | commandArgs.push('--cache-from', cacheFrom), 243 | ); 244 | } 245 | if (args.cacheTo) { 246 | args.cacheTo.forEach(cacheTo => 247 | commandArgs.push('--cache-to', cacheTo), 248 | ); 249 | } 250 | if (args.userDataFolder) { 251 | commandArgs.push("--user-data-folder", args.userDataFolder); 252 | } 253 | if (args.skipContainerUserIdUpdate) { 254 | commandArgs.push('--update-remote-user-uid-default', 'off'); 255 | } 256 | if (args.additionalMounts) { 257 | args.additionalMounts.forEach(mount => 258 | commandArgs.push('--mount', mount), 259 | ); 260 | } 261 | return await runSpecCliJsonCommand({ 262 | args: commandArgs, 263 | log, 264 | env: {DOCKER_BUILDKIT: '1', COMPOSE_DOCKER_CLI_BUILD: '1'}, 265 | }); 266 | } 267 | 268 | export interface DevContainerCliExecArgs { 269 | workspaceFolder: string; 270 | configFile: string | undefined; 271 | command: string[]; 272 | env?: string[]; 273 | userDataFolder?: string; 274 | } 275 | async function devContainerExec( 276 | args: DevContainerCliExecArgs, 277 | log: (data: string) => void, 278 | ): Promise { 279 | // const remoteEnvArgs = args.env ? args.env.flatMap(e=> ["--remote-env", e]): []; // TODO - test flatMap again 280 | const remoteEnvArgs = getRemoteEnvArray(args.env); 281 | const commandArgs = ["exec", "--workspace-folder", args.workspaceFolder, ...remoteEnvArgs]; 282 | if (args.configFile) { 283 | commandArgs.push('--config', args.configFile); 284 | } 285 | if (args.userDataFolder) { 286 | commandArgs.push("--user-data-folder", args.userDataFolder); 287 | } 288 | return await runSpecCliNonJsonCommand({ 289 | args: commandArgs.concat(args.command), 290 | log, 291 | env: {DOCKER_BUILDKIT: '1', COMPOSE_DOCKER_CLI_BUILD: '1'}, 292 | }); 293 | } 294 | 295 | function getRemoteEnvArray(env?: string[]): string[] { 296 | if (!env) { 297 | return []; 298 | } 299 | let result = []; 300 | for (let i = 0; i < env.length; i++) { 301 | const envItem = env[i]; 302 | result.push('--remote-env', envItem); 303 | } 304 | return result; 305 | } 306 | 307 | export const devcontainer = { 308 | build: devContainerBuild, 309 | up: devContainerUp, 310 | exec: devContainerExec, 311 | isCliInstalled, 312 | installCli, 313 | }; 314 | -------------------------------------------------------------------------------- /common/src/envvars.ts: -------------------------------------------------------------------------------- 1 | export function substituteValues(input: string): string { 2 | // Find all `${...}` entries and substitute 3 | // Note the non-greedy `.+?` match to avoid matching the start of 4 | // one placeholder up to the end of another when multiple placeholders are present 5 | return input.replace(/\$\{(.+?)\}/g, getSubstitutionValue); 6 | } 7 | 8 | function getSubstitutionValue(regexMatch: string, placeholder: string): string { 9 | // Substitution values are in TYPE:KEY form 10 | // e.g. env:MY_ENV 11 | 12 | const parts = placeholder.split(':'); 13 | if (parts.length === 2) { 14 | const type = parts[0]; 15 | const key = parts[1]; 16 | switch (type.toLowerCase()) { 17 | case 'env': 18 | case 'localenv': 19 | return process.env[key] ?? ''; 20 | } 21 | } 22 | 23 | // if we can't process the format then return the original string 24 | // as having it present in any output will likely make issues more obvious 25 | return regexMatch; 26 | } 27 | 28 | // populateDefaults expects strings either "FOO=hello" or "BAR". 29 | // In the latter case, the corresponding returned item would be "BAR=hi" 30 | // where the value is taken from the matching process env var. 31 | // In the case of values not set in the process, they are omitted 32 | export function populateDefaults(envs: string[], inheritEnv: boolean): string[] { 33 | const result: string[] = []; 34 | if (inheritEnv) { 35 | for (const [key, value] of Object.entries(process.env)) { 36 | switch (key) { 37 | case 'PATH': 38 | // don't copy these by default (user can still explicitly specify them). 39 | break; 40 | default: 41 | result.push(`${key}=${value}`); 42 | break; 43 | } 44 | } 45 | } 46 | for (let i = 0; i < envs.length; i++) { 47 | const inputEnv = envs[i]; 48 | if (inputEnv.indexOf('=') >= 0) { 49 | // pass straight through to result 50 | result.push(inputEnv); 51 | } else { 52 | // inputEnv is just the env var name 53 | const processEnvValue = process.env[inputEnv]; 54 | if (processEnvValue) { 55 | result.push(`${inputEnv}=${processEnvValue}`); 56 | } 57 | } 58 | } 59 | return result; 60 | } 61 | -------------------------------------------------------------------------------- /common/src/exec.ts: -------------------------------------------------------------------------------- 1 | export interface ExecResult { 2 | exitCode: number; 3 | stdout: string; 4 | stderr: string; 5 | } 6 | export interface ExecOptions { 7 | silent?: boolean; 8 | } 9 | export type ExecFunction = ( 10 | command: string, 11 | args: string[], 12 | options: ExecOptions, 13 | ) => Promise; 14 | -------------------------------------------------------------------------------- /common/src/file.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export function getAbsolutePath( 4 | inputPath: string, 5 | referencePath: string, 6 | ): string { 7 | if (path.isAbsolute(inputPath)) { 8 | return inputPath; 9 | } 10 | return path.join(referencePath, inputPath); 11 | } 12 | -------------------------------------------------------------------------------- /common/src/skopeo.ts: -------------------------------------------------------------------------------- 1 | import {ExecFunction} from './exec'; 2 | 3 | export async function isSkopeoInstalled( 4 | exec: ExecFunction, 5 | ): Promise { 6 | const {exitCode} = await exec('skopeo', ['--help'], {silent: true}); 7 | return exitCode === 0; 8 | } 9 | 10 | export async function copyImage( 11 | exec: ExecFunction, 12 | all: boolean, 13 | source: string, 14 | dest: string, 15 | ): Promise { 16 | const args = ['copy']; 17 | if (all) { 18 | args.push('--all'); 19 | } 20 | args.push(source, dest); 21 | 22 | const {exitCode} = await exec('skopeo', args, {}); 23 | 24 | if (exitCode !== 0) { 25 | throw new Error(`skopeo copy failed with ${exitCode}`); 26 | } 27 | } -------------------------------------------------------------------------------- /common/src/users.ts: -------------------------------------------------------------------------------- 1 | interface User { 2 | name: string; 3 | // omitted password 4 | uid: string; 5 | gid: string; 6 | // omitted GECOS, 7 | // omitted home dir 8 | // omitted login shell 9 | } 10 | export function parsePasswd(input: string): User[] { 11 | const result: User[] = []; 12 | const lines = input.split('\n'); 13 | for (const line of lines) { 14 | const parts = line.split(':'); 15 | const user: User = { 16 | name: parts[0], 17 | uid: parts[2], 18 | gid: parts[3], 19 | }; 20 | result.push(user); 21 | } 22 | return result; 23 | } 24 | 25 | interface Group { 26 | name: string; 27 | // omitted password 28 | gid: string; 29 | users: string[]; 30 | } 31 | 32 | export function parseGroup(input: string): Group[] { 33 | const result: Group[] = []; 34 | const lines = input.split('\n'); 35 | for (const line of lines) { 36 | const parts = line.split(':'); 37 | const group: Group = { 38 | name: parts[0], 39 | gid: parts[2], 40 | users: parts[3] ? parts[3].split(',') : [], 41 | }; 42 | result.push(group); 43 | } 44 | return result; 45 | } 46 | -------------------------------------------------------------------------------- /common/src/windows.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | import * as path from 'path'; 7 | import * as fs from 'fs'; 8 | 9 | // From Dev Containers CLI 10 | export async function findWindowsExecutable(command: string): Promise { 11 | if (process.platform !== 'win32') { 12 | return command; 13 | } 14 | 15 | // If we have an absolute path then we take it. 16 | if (path.isAbsolute(command)) { 17 | return await findWindowsExecutableWithExtension(command) || command; 18 | } 19 | const cwd = process.cwd(); 20 | if (/[/\\]/.test(command)) { 21 | // We have a directory and the directory is relative (see above). Make the path absolute 22 | // to the current working directory. 23 | const fullPath = path.join(cwd, command); 24 | return await findWindowsExecutableWithExtension(fullPath) || fullPath; 25 | } 26 | let pathValue: string | undefined = undefined; 27 | let paths: string[] | undefined = undefined; 28 | const env = process.env; 29 | // Path can be named in many different ways and for the execution it doesn't matter 30 | for (let key of Object.keys(env)) { 31 | if (key.toLowerCase() === 'path') { 32 | const value = env[key]; 33 | if (typeof value === 'string') { 34 | pathValue = value; 35 | paths = value.split(path.delimiter) 36 | .filter(Boolean); 37 | } 38 | break; 39 | } 40 | } 41 | // No PATH environment. Bail out. 42 | if (paths === void 0 || paths.length === 0) { 43 | const err = new Error(`No PATH to look up executable '${command}'.`); 44 | (err as any).code = 'ENOENT'; 45 | throw err; 46 | } 47 | // We have a simple file name. We get the path variable from the env 48 | // and try to find the executable on the path. 49 | for (let pathEntry of paths) { 50 | // The path entry is absolute. 51 | let fullPath: string; 52 | if (path.isAbsolute(pathEntry)) { 53 | fullPath = path.join(pathEntry, command); 54 | } else { 55 | fullPath = path.join(cwd, pathEntry, command); 56 | } 57 | const withExtension = await findWindowsExecutableWithExtension(fullPath); 58 | if (withExtension) { 59 | return withExtension; 60 | } 61 | } 62 | // Not found in PATH. Bail out. 63 | const err = new Error(`Exectuable '${command}' not found on PATH '${pathValue}'.`); 64 | (err as any).code = 'ENOENT'; 65 | throw err; 66 | } 67 | 68 | const pathext = process.env.PATHEXT; 69 | const executableExtensions = pathext ? pathext.toLowerCase().split(';') : ['.com', '.exe', '.bat', '.cmd']; 70 | 71 | async function findWindowsExecutableWithExtension(fullPath: string) { 72 | if (executableExtensions.indexOf(path.extname(fullPath)) !== -1) { 73 | return await isFile(fullPath) ? fullPath : undefined; 74 | } 75 | for (const ext of executableExtensions) { 76 | const withExtension = fullPath + ext; 77 | if (await isFile(withExtension)) { 78 | return withExtension; 79 | } 80 | } 81 | return undefined; 82 | } 83 | 84 | function isFile(filepath: string): Promise { 85 | return new Promise(r => fs.stat(filepath, (err, stat) => r(!err && stat.isFile()))); 86 | } 87 | -------------------------------------------------------------------------------- /common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "../common_lib", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "esModuleInterop": true, 10 | "moduleResolution": "node", 11 | "composite": true, 12 | "declaration": true 13 | }, 14 | "exclude": ["node_modules", "**/*.test.ts"], 15 | } 16 | -------------------------------------------------------------------------------- /docs/multi-platform-builds.md: -------------------------------------------------------------------------------- 1 | # Multiplatform Dev Container Builds 2 | 3 | Building dev containers to support multiple platforms (aka CPU architectures) is possible with the devcontainers/ci GitHub Action/Azure DevOps Task, but requires other actions/tasks to be run beforehand and has several caveats. 4 | 5 | ## General Notes/Caveats 6 | 7 | - Multiplatform builds utilize emulation to build on architectures not native to the system the build is running on. This will significantly increase build times over native, single architecture builds. 8 | - If you are using runCmd, the command will only be run on the architecure of the system the build is running on. This means that, if you are using runCmd to test the image, there may be bugs on the alternate platforms that will not be caught by your test suite. Manual post-build testing is advised. 9 | - As of October 2022, all hosted servers for GitHub Actions and Azure Pipelines are x86_64 only. If you want to automatically run runCmd-based tests on your devcontainer on another architecure, you'll need a self-hosted runner on that architecture. It is possible that there will be future support for hosted arm64 machines, see [here for a tracking issue for Linux](https://github.com/actions/runner-images/issues/5631). 10 | 11 | ## GitHub Actions Example 12 | 13 | ``` 14 | name: 'build' 15 | on: 16 | pull_request: 17 | push: 18 | branches: 19 | - main 20 | 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout (GitHub) 26 | uses: actions/checkout@v3 27 | - name: Set up QEMU for multi-architecture builds 28 | uses: docker/setup-qemu-action@v3 29 | - name: Setup Docker buildx for multi-architecture builds 30 | uses: docker/setup-buildx-action@v3 31 | with: 32 | use: true 33 | - name: Login to GitHub Container Registry 34 | uses: docker/login-action@v2 35 | with: 36 | registry: ghcr.io 37 | username: ${{ github.repository_owner }} 38 | password: ${{ secrets.GITHUB_TOKEN }} 39 | - name: Build and release devcontainer Multi-Platform 40 | uses: devcontainers/ci@v0.3 41 | with: 42 | imageName: ghcr.io/UserNameHere/ImageNameHere 43 | platform: linux/amd64,linux/arm64 44 | ``` 45 | 46 | ## Azure DevOps Task Example 47 | 48 | ``` 49 | trigger: 50 | - main 51 | 52 | pool: 53 | vmImage: ubuntu-latest 54 | 55 | jobs: 56 | - job: BuildContainerImage 57 | displayName: Build Container Image 58 | timeoutInMinutes: 0 59 | steps: 60 | - checkout: self 61 | - task: Docker@2 62 | displayName: Login to Container Registry 63 | inputs: 64 | command: login 65 | containerRegistry: RegistryNameHere 66 | - script: docker run --rm --privileged multiarch/qemu-user-static --reset -p yes 67 | displayName: Set up QEMU 68 | - script: docker buildx create --use 69 | displayName: Set up docker buildx 70 | - task: DevcontainersCi@0 71 | inputs: 72 | imageName: UserNameHere/ImageNameHere 73 | platform: linux/amd64,linux/arm64 74 | ``` 75 | -------------------------------------------------------------------------------- /github-action/.eslintignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ 4 | jest.config.js 5 | -------------------------------------------------------------------------------- /github-action/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["jest", "@typescript-eslint"], 3 | "extends": ["plugin:github/recommended"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "ecmaVersion": 9, 7 | "sourceType": "module", 8 | "project": "./tsconfig.json" 9 | }, 10 | "rules": { 11 | "eslint-comments/no-use": "off", 12 | "import/no-namespace": "off", 13 | "i18n-text/no-en": "off", 14 | "no-unused-vars": "off", 15 | "@typescript-eslint/no-unused-vars": "error", 16 | "@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}], 17 | "@typescript-eslint/no-require-imports": "error", 18 | "@typescript-eslint/array-type": "error", 19 | "@typescript-eslint/await-thenable": "error", 20 | "@typescript-eslint/ban-ts-comment": "error", 21 | "camelcase": "off", 22 | "@typescript-eslint/consistent-type-assertions": "error", 23 | "@typescript-eslint/explicit-function-return-type": ["error", {"allowExpressions": true}], 24 | "func-call-spacing": ["error", "never"], 25 | "@typescript-eslint/no-array-constructor": "error", 26 | "@typescript-eslint/no-empty-interface": "error", 27 | "@typescript-eslint/no-explicit-any": "error", 28 | "@typescript-eslint/no-extraneous-class": "error", 29 | "@typescript-eslint/no-for-in-array": "error", 30 | "@typescript-eslint/no-inferrable-types": "error", 31 | "@typescript-eslint/no-misused-new": "error", 32 | "@typescript-eslint/no-namespace": "error", 33 | "@typescript-eslint/no-non-null-assertion": "warn", 34 | "@typescript-eslint/no-unnecessary-qualifier": "error", 35 | "@typescript-eslint/no-unnecessary-type-assertion": "error", 36 | "@typescript-eslint/no-useless-constructor": "error", 37 | "@typescript-eslint/no-var-requires": "error", 38 | "@typescript-eslint/prefer-for-of": "warn", 39 | "@typescript-eslint/prefer-function-type": "warn", 40 | "@typescript-eslint/prefer-includes": "error", 41 | "@typescript-eslint/prefer-string-starts-ends-with": "error", 42 | "@typescript-eslint/promise-function-async": "error", 43 | "@typescript-eslint/require-array-sort-compare": "error", 44 | "@typescript-eslint/restrict-plus-operands": "error", 45 | "semi": ["error", "always"], 46 | "@typescript-eslint/unbound-method": "error" 47 | }, 48 | "env": { 49 | "node": true, 50 | "es6": true, 51 | "jest/globals": true 52 | } 53 | } -------------------------------------------------------------------------------- /github-action/.prettierignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | lib/ 3 | node_modules/ -------------------------------------------------------------------------------- /github-action/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": true, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid" 10 | } 11 | -------------------------------------------------------------------------------- /github-action/Makefile: -------------------------------------------------------------------------------- 1 | help: ## show this help 2 | @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ 3 | | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%s\033[0m|%s\n", $$1, $$2}' \ 4 | | column -t -s '|' 5 | 6 | npm-install: 7 | npm install 8 | 9 | lint: 10 | npm run lint 11 | 12 | lint-fix: 13 | npm run lint-fix 14 | 15 | build-package: 16 | npm run build && npm run package 17 | 18 | test: build-package 19 | npm test 20 | -------------------------------------------------------------------------------- /github-action/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | moduleFileExtensions: ['js', 'ts'], 4 | testEnvironment: 'node', 5 | testMatch: ['**/*.test.ts'], 6 | testRunner: 'jest-circus/runner', 7 | transform: { 8 | '^.+\\.ts$': 'ts-jest' 9 | }, 10 | verbose: true 11 | } -------------------------------------------------------------------------------- /github-action/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devcontainer-build-run", 3 | "version": "0.0.0", 4 | "private": true, 5 | "description": "Action to simplify using Dev Containers (https://containers.dev) in GitHub workflows", 6 | "main": "lib/main.js", 7 | "scripts": { 8 | "tsc-version": "tsc --version", 9 | "build": "tsc --build", 10 | "format": "prettier --write **/*.ts", 11 | "format-check": "prettier --check **/*.ts", 12 | "lint": "eslint src/**/*.ts", 13 | "lint-fix": "eslint --fix src/**/*.ts", 14 | "package": "ncc build --source-map --license licenses.txt", 15 | "test": "jest", 16 | "all": "npm run tsc-version && npm run build && npm run format && npm run lint && npm run package #&& npm test" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/devcontainers/ci.git" 21 | }, 22 | "keywords": [ 23 | "actions", 24 | "node", 25 | "setup" 26 | ], 27 | "author": "", 28 | "license": "MIT", 29 | "dependencies": { 30 | "@actions/core": "^1.11.1", 31 | "@actions/exec": "^1.1.1", 32 | "jsonc-parser": "^3.3.1", 33 | "truncate-utf8-bytes": "^1.0.2" 34 | }, 35 | "devDependencies": { 36 | "@types/jest": "^29.5.14", 37 | "@types/node": "^22.14.1", 38 | "@typescript-eslint/eslint-plugin": "^8.31.0", 39 | "@typescript-eslint/parser": "^8.31.0", 40 | "@vercel/ncc": "^0.38.3", 41 | "eslint": "^8.57.0", 42 | "eslint-plugin-github": "^5.1.8", 43 | "eslint-plugin-jest": "^28.11.0", 44 | "jest": "^29.7.0", 45 | "jest-circus": "^29.7.0", 46 | "js-yaml": "^4.1.0", 47 | "prettier": "^3.5.3", 48 | "ts-jest": "^29.3.2", 49 | "typescript": "^5.8.3", 50 | "@types/truncate-utf8-bytes": "^1.0.2" 51 | } 52 | } -------------------------------------------------------------------------------- /github-action/run-main.js: -------------------------------------------------------------------------------- 1 | const { runMain } = require("./dist/index.js"); 2 | 3 | runMain(); -------------------------------------------------------------------------------- /github-action/run-post.js: -------------------------------------------------------------------------------- 1 | const { runPost } = require("./dist/index.js"); 2 | 3 | runPost(); -------------------------------------------------------------------------------- /github-action/src/docker.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as docker from '../../common/src/docker'; 3 | import {exec} from './exec'; 4 | 5 | export async function isDockerBuildXInstalled(): Promise { 6 | return await docker.isDockerBuildXInstalled(exec); 7 | } 8 | export async function buildImage( 9 | imageName: string, 10 | imageTag: string | undefined, 11 | checkoutPath: string, 12 | subFolder: string, 13 | skipContainerUserIdUpdate: boolean, 14 | cacheFrom: string[], 15 | cacheTo: string[], 16 | ): Promise { 17 | core.startGroup('🏗 Building dev container...'); 18 | try { 19 | return await docker.buildImage( 20 | exec, 21 | imageName, 22 | imageTag, 23 | checkoutPath, 24 | subFolder, 25 | skipContainerUserIdUpdate, 26 | cacheFrom, 27 | cacheTo, 28 | ); 29 | } catch (error) { 30 | core.setFailed(error); 31 | return ''; 32 | } finally { 33 | core.endGroup(); 34 | } 35 | } 36 | 37 | export async function runContainer( 38 | imageName: string, 39 | imageTag: string | undefined, 40 | checkoutPath: string, 41 | subFolder: string, 42 | command: string, 43 | envs?: string[], 44 | ): Promise { 45 | core.startGroup('🏃‍♀️ Running dev container...'); 46 | try { 47 | await docker.runContainer( 48 | exec, 49 | imageName, 50 | imageTag, 51 | checkoutPath, 52 | subFolder, 53 | command, 54 | envs, 55 | ); 56 | return true; 57 | } catch (error) { 58 | core.setFailed(error); 59 | return false; 60 | } finally { 61 | core.endGroup(); 62 | } 63 | } 64 | 65 | export async function pushImage( 66 | imageName: string, 67 | imageTag: string | undefined, 68 | ): Promise { 69 | core.startGroup('📌 Pushing image...'); 70 | try { 71 | await docker.pushImage(exec, imageName, imageTag); 72 | return true; 73 | } catch (error) { 74 | core.setFailed(error); 75 | return false; 76 | } finally { 77 | core.endGroup(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /github-action/src/exec.ts: -------------------------------------------------------------------------------- 1 | import * as actions_exec from '@actions/exec'; 2 | import {ExecOptions, ExecResult} from '../../common/src/exec'; 3 | 4 | export async function exec( 5 | command: string, 6 | args: string[], 7 | options: ExecOptions, 8 | ): Promise { 9 | const actionOptions: actions_exec.ExecOptions = { 10 | ignoreReturnCode: true, 11 | silent: options.silent ?? false, 12 | }; 13 | const result = await actions_exec.getExecOutput(command, args, actionOptions); 14 | 15 | return { 16 | exitCode: result.exitCode, 17 | stdout: result.stdout, 18 | stderr: result.stderr, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /github-action/src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import truncate from 'truncate-utf8-bytes'; 3 | import path from 'path'; 4 | import {exec} from './exec'; 5 | import { 6 | devcontainer, 7 | DevContainerCliBuildArgs, 8 | DevContainerCliExecArgs, 9 | DevContainerCliUpArgs, 10 | } from '../../common/src/dev-container-cli'; 11 | 12 | import {isDockerBuildXInstalled, pushImage} from './docker'; 13 | import {isSkopeoInstalled, copyImage} from './skopeo'; 14 | import {populateDefaults} from '../../common/src/envvars'; 15 | 16 | // List the env vars that point to paths to mount in the dev container 17 | // See https://docs.github.com/en/actions/learn-github-actions/variables 18 | const githubEnvs = { 19 | GITHUB_OUTPUT: '/mnt/github/output', 20 | GITHUB_ENV: '/mnt/github/env', 21 | GITHUB_PATH: '/mnt/github/path', 22 | GITHUB_STEP_SUMMARY: '/mnt/github/step-summary', 23 | }; 24 | 25 | export async function runMain(): Promise { 26 | try { 27 | core.info('Starting...'); 28 | core.saveState('hasRunMain', 'true'); 29 | const buildXInstalled = await isDockerBuildXInstalled(); 30 | if (!buildXInstalled) { 31 | core.warning( 32 | 'docker buildx not available: add a step to set up with docker/setup-buildx-action - see https://github.com/devcontainers/ci/blob/main/docs/github-action.md', 33 | ); 34 | return; 35 | } 36 | const devContainerCliInstalled = await devcontainer.isCliInstalled(exec); 37 | if (!devContainerCliInstalled) { 38 | core.info('Installing @devcontainers/cli...'); 39 | const success = await devcontainer.installCli(exec); 40 | if (!success) { 41 | core.setFailed('@devcontainers/cli install failed!'); 42 | return; 43 | } 44 | } 45 | 46 | const checkoutPath: string = core.getInput('checkoutPath'); 47 | const imageName = emptyStringAsUndefined(core.getInput('imageName')); 48 | const imageTag = emptyStringAsUndefined(core.getInput('imageTag')); 49 | const platform = emptyStringAsUndefined(core.getInput('platform')); 50 | const subFolder: string = core.getInput('subFolder'); 51 | const relativeConfigFile = emptyStringAsUndefined( 52 | core.getInput('configFile'), 53 | ); 54 | const runCommand = core.getInput('runCmd'); 55 | const inputEnvs: string[] = core.getMultilineInput('env'); 56 | const inheritEnv: boolean = core.getBooleanInput('inheritEnv'); 57 | const inputEnvsWithDefaults = populateDefaults(inputEnvs, inheritEnv); 58 | const cacheFrom: string[] = core.getMultilineInput('cacheFrom'); 59 | const noCache: boolean = core.getBooleanInput('noCache'); 60 | const cacheTo: string[] = core.getMultilineInput('cacheTo'); 61 | const skipContainerUserIdUpdate = core.getBooleanInput( 62 | 'skipContainerUserIdUpdate', 63 | ); 64 | const userDataFolder: string = core.getInput('userDataFolder'); 65 | const mounts: string[] = core.getMultilineInput('mounts'); 66 | 67 | if (platform) { 68 | const skopeoInstalled = await isSkopeoInstalled(); 69 | if (!skopeoInstalled) { 70 | core.warning( 71 | 'skopeo not available and is required for multi-platform builds - make sure it is installed on your runner', 72 | ); 73 | return; 74 | } 75 | } 76 | const buildxOutput = platform ? 'type=oci,dest=/tmp/output.tar' : undefined; 77 | 78 | const log = (message: string): void => core.info(message); 79 | const workspaceFolder = path.resolve(checkoutPath, subFolder); 80 | const configFile = 81 | relativeConfigFile && path.resolve(checkoutPath, relativeConfigFile); 82 | 83 | const resolvedImageTag = imageTag ?? 'latest'; 84 | const imageTagArray = resolvedImageTag.split(/\s*,\s*/); 85 | const fullImageNameArray: string[] = []; 86 | for (const tag of imageTagArray) { 87 | fullImageNameArray.push(`${imageName}:${tag}`); 88 | } 89 | if (imageName) { 90 | if (fullImageNameArray.length === 1) { 91 | if (!noCache && !cacheFrom.includes(fullImageNameArray[0])) { 92 | // If the cacheFrom options don't include the fullImageName, add it here 93 | // This ensures that when building a PR where the image specified in the action 94 | // isn't included in devcontainer.json (or docker-compose.yml), the action still 95 | // resolves a previous image for the tag as a layer cache (if pushed to a registry) 96 | 97 | core.info( 98 | `Adding --cache-from ${fullImageNameArray[0]} to build args`, 99 | ); 100 | cacheFrom.splice(0, 0, fullImageNameArray[0]); 101 | } 102 | } else { 103 | // Don't automatically add --cache-from if multiple image tags are specified 104 | core.info( 105 | 'Not adding --cache-from automatically since multiple image tags were supplied', 106 | ); 107 | } 108 | } else { 109 | if (imageTag) { 110 | core.warning( 111 | 'imageTag specified without specifying imageName - ignoring imageTag', 112 | ); 113 | } 114 | } 115 | const buildResult = await core.group('🏗️ build container', async () => { 116 | const args: DevContainerCliBuildArgs = { 117 | workspaceFolder, 118 | configFile, 119 | imageName: fullImageNameArray, 120 | platform, 121 | additionalCacheFroms: cacheFrom, 122 | userDataFolder, 123 | output: buildxOutput, 124 | noCache, 125 | cacheTo, 126 | }; 127 | const result = await devcontainer.build(args, log); 128 | 129 | if (result.outcome !== 'success') { 130 | core.error( 131 | `Dev container build failed: ${result.message} (exit code: ${result.code})\n${result.description}`, 132 | ); 133 | core.setFailed(result.message); 134 | } 135 | return result; 136 | }); 137 | if (buildResult.outcome !== 'success') { 138 | return; 139 | } 140 | 141 | for (const [key, value] of Object.entries(githubEnvs)) { 142 | if (process.env[key]) { 143 | // Add additional bind mount 144 | mounts.push(`type=bind,source=${process.env[key]},target=${value}`); 145 | // Set env var to mounted path in container 146 | inputEnvsWithDefaults.push(`${key}=${value}`); 147 | } 148 | } 149 | 150 | if (runCommand) { 151 | const upResult = await core.group('🏃 start container', async () => { 152 | const args: DevContainerCliUpArgs = { 153 | workspaceFolder, 154 | configFile, 155 | additionalCacheFroms: cacheFrom, 156 | skipContainerUserIdUpdate, 157 | env: inputEnvsWithDefaults, 158 | userDataFolder, 159 | additionalMounts: mounts, 160 | }; 161 | const result = await devcontainer.up(args, log); 162 | if (result.outcome !== 'success') { 163 | core.error( 164 | `Dev container up failed: ${result.message} (exit code: ${result.code})\n${result.description}`, 165 | ); 166 | core.setFailed(result.message); 167 | } 168 | return result; 169 | }); 170 | if (upResult.outcome !== 'success') { 171 | return; 172 | } 173 | 174 | const args: DevContainerCliExecArgs = { 175 | workspaceFolder, 176 | configFile, 177 | command: ['bash', '-c', runCommand], 178 | env: inputEnvsWithDefaults, 179 | userDataFolder, 180 | }; 181 | let execLogString = ''; 182 | const execLog = (message: string): void => { 183 | core.info(message); 184 | if (!message.includes('@devcontainers/cli')) { 185 | execLogString += message; 186 | } 187 | }; 188 | const exitCode = await devcontainer.exec(args, execLog); 189 | if (exitCode !== 0) { 190 | const errorMessage = `Dev container exec failed: (exit code: ${exitCode})`; 191 | core.error(errorMessage); 192 | core.setFailed(errorMessage); 193 | } 194 | core.setOutput('runCmdOutput', execLogString); 195 | if (Buffer.byteLength(execLogString, 'utf-8') > 1000000) { 196 | execLogString = truncate(execLogString, 999966); 197 | execLogString += 'TRUNCATED TO 1 MB MAX OUTPUT SIZE'; 198 | } 199 | core.setOutput('runCmdOutput', execLogString); 200 | if (exitCode !== 0) { 201 | return; 202 | } 203 | } else { 204 | core.info('No runCmd set - skipping starting/running container'); 205 | } 206 | 207 | // TODO - should we stop the container? 208 | } catch (error) { 209 | core.setFailed(error.message); 210 | } 211 | } 212 | 213 | export async function runPost(): Promise { 214 | const pushOption = emptyStringAsUndefined(core.getInput('push')); 215 | const imageName = emptyStringAsUndefined(core.getInput('imageName')); 216 | const refFilterForPush: string[] = core.getMultilineInput('refFilterForPush'); 217 | const eventFilterForPush: string[] = 218 | core.getMultilineInput('eventFilterForPush'); 219 | 220 | // default to 'never' if not set and no imageName 221 | if (pushOption === 'never' || (!pushOption && !imageName)) { 222 | core.info(`Image push skipped because 'push' is set to '${pushOption}'`); 223 | return; 224 | } 225 | 226 | // default to 'filter' if not set and imageName is set 227 | if (pushOption === 'filter' || (!pushOption && imageName)) { 228 | // https://docs.github.com/en/actions/reference/environment-variables#default-environment-variables 229 | const ref = process.env.GITHUB_REF; 230 | if ( 231 | refFilterForPush.length !== 0 && // empty filter allows all 232 | !refFilterForPush.some(s => s === ref) 233 | ) { 234 | core.info( 235 | `Image push skipped because GITHUB_REF (${ref}) is not in refFilterForPush`, 236 | ); 237 | return; 238 | } 239 | const eventName = process.env.GITHUB_EVENT_NAME; 240 | if ( 241 | eventFilterForPush.length !== 0 && // empty filter allows all 242 | !eventFilterForPush.some(s => s === eventName) 243 | ) { 244 | core.info( 245 | `Image push skipped because GITHUB_EVENT_NAME (${eventName}) is not in eventFilterForPush`, 246 | ); 247 | return; 248 | } 249 | } else if (pushOption !== 'always') { 250 | core.setFailed(`Unexpected push value ('${pushOption})'`); 251 | return; 252 | } 253 | 254 | const imageTag = 255 | emptyStringAsUndefined(core.getInput('imageTag')) ?? 'latest'; 256 | const imageTagArray = imageTag.split(/\s*,\s*/); 257 | if (!imageName) { 258 | if (pushOption) { 259 | // pushOption was set (and not to "never") - give an error that imageName is required 260 | core.error('imageName is required to push images'); 261 | } 262 | return; 263 | } 264 | 265 | const platform = emptyStringAsUndefined(core.getInput('platform')); 266 | if (platform) { 267 | for (const tag of imageTagArray) { 268 | core.info(`Copying multiplatform image '${imageName}:${tag}'...`); 269 | const imageSource = `oci-archive:/tmp/output.tar:${tag}`; 270 | const imageDest = `docker://${imageName}:${tag}`; 271 | 272 | await copyImage(true, imageSource, imageDest); 273 | } 274 | } else { 275 | for (const tag of imageTagArray) { 276 | core.info(`Pushing image '${imageName}:${tag}'...`); 277 | await pushImage(imageName, tag); 278 | } 279 | } 280 | } 281 | 282 | function emptyStringAsUndefined(value: string): string | undefined { 283 | if (value === '') { 284 | return undefined; 285 | } 286 | return value; 287 | } 288 | -------------------------------------------------------------------------------- /github-action/src/skopeo.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as skopeo from '../../common/src/skopeo'; 3 | import {exec} from './exec'; 4 | 5 | export async function isSkopeoInstalled(): Promise { 6 | return await skopeo.isSkopeoInstalled(exec); 7 | } 8 | 9 | export async function copyImage( 10 | all: boolean, 11 | source: string, 12 | dest: string, 13 | ): Promise { 14 | core.startGroup('📌 Copying image...'); 15 | try { 16 | await skopeo.copyImage(exec, all, source, dest); 17 | return true; 18 | } catch (error) { 19 | core.setFailed(error); 20 | return false; 21 | } finally { 22 | core.endGroup(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /github-action/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "./lib", 6 | "rootDir": "./src", 7 | "strict": true, 8 | "useUnknownInCatchVariables": false, 9 | "noImplicitAny": true, 10 | "esModuleInterop": true, 11 | "moduleResolution": "node" 12 | }, 13 | "exclude": ["node_modules", "**/*.test.ts"], 14 | "references": [ 15 | { "path": "../common"} 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/build-args/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian / Ubuntu version: debian-10, debian-9, ubuntu-20.04, ubuntu-18.04 2 | # See https://github.com/microsoft/vscode-dev-containers/tree/master/containers/debian 3 | ARG VARIANT=debian-12 4 | FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT} 5 | 6 | 7 | # Avoid warnings by switching to noninteractive 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | 10 | # Set env for tracking that we're running in a devcontainer 11 | ENV DEVCONTAINER=true 12 | 13 | # This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" 14 | # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs 15 | # will be updated to match your local UID/GID (when using the dockerFile property). 16 | # See https://aka.ms/vscode-remote/containers/non-root-user for details. 17 | ARG USERNAME=vscode 18 | ARG USER_UID=1000 19 | ARG USER_GID=$USER_UID 20 | 21 | USER $USERNAME 22 | RUN \ 23 | mkdir -p ~/.local/bin \ 24 | && echo "export PATH=\$PATH:~/.local/bin" >> ~/.bashrc 25 | 26 | # Configure apt, install packages and general tools 27 | RUN sudo apt-get update \ 28 | && sudo apt-get -y install --no-install-recommends apt-utils dialog nano bash-completion sudo bsdmainutils \ 29 | # 30 | # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed 31 | && sudo apt-get -y install git iproute2 procps lsb-release figlet build-essential 32 | 33 | # Save command line history 34 | RUN echo "export HISTFILE=/home/$USERNAME/commandhistory/.bash_history" >> "/home/$USERNAME/.bashrc" \ 35 | && echo "export PROMPT_COMMAND='history -a'" >> "/home/$USERNAME/.bashrc" \ 36 | && mkdir -p /home/$USERNAME/commandhistory \ 37 | && touch /home/$USERNAME/commandhistory/.bash_history \ 38 | && chown -R $USERNAME /home/$USERNAME/commandhistory 39 | 40 | # Set env for tracking that we're running in a devcontainer 41 | ENV DEVCONTAINER=true 42 | 43 | ARG TEST_ARG 44 | ENV BUILD_ARG_TEST=$TEST_ARG 45 | 46 | # docker-client 47 | COPY scripts/docker-client.sh /tmp/ 48 | RUN /tmp/docker-client.sh 49 | 50 | #Add user to docker group 51 | RUN sudo groupadd docker && sudo usermod -aG docker $USERNAME && newgrp docker 52 | 53 | # __DEVCONTAINER_SNIPPET_INSERT__ (control where snippets get inserted using the devcontainer CLI) 54 | 55 | # Switch back to dialog for any ad-hoc use of apt-get 56 | ENV DEBIAN_FRONTEND=dialog 57 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/build-args/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "build-args", 5 | "dockerFile": "Dockerfile", 6 | "build": { 7 | "args": { 8 | "TEST_ARG": "Hello build-args!" 9 | } 10 | }, 11 | 12 | "mounts": [ 13 | // Keep command history 14 | "source=build-args-bashhistory,target=/home/vscode/commandhistory", 15 | // Mount host docker socket 16 | "type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock" 17 | 18 | ], 19 | 20 | // Set *default* container specific settings.json values on container create. 21 | "settings": { 22 | "terminal.integrated.shell.linux": "/bin/bash", 23 | "files.eol": "\n", 24 | }, 25 | 26 | // Add the IDs of extensions you want installed when the container is created. 27 | // "extensions": [], 28 | 29 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 30 | // "forwardPorts": [], 31 | 32 | // Use 'postCreateCommand' to run commands after the container is created. 33 | // "postCreateCommand": "echo hello", 34 | 35 | "remoteUser": "vscode", 36 | "extensions": [ 37 | "ms-azuretools.vscode-docker", 38 | ] 39 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/build-args/.devcontainer/scripts/docker-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | VERSION=${1:-"20.10.5"} 5 | INSTALL_DIR=${2:-"$HOME/.local/bin"} 6 | CMD=docker 7 | NAME="Docker Client" 8 | 9 | echo -e "\e[34m»»» 📦 \e[32mInstalling \e[33m$NAME \e[35mv$VERSION\e[0m ..." 10 | 11 | mkdir -p $INSTALL_DIR 12 | curl -sSL https://download.docker.com/linux/static/stable/x86_64/docker-$VERSION.tgz -o /tmp/docker.tgz 13 | tar -zxvf /tmp/docker.tgz -C /tmp docker/docker 14 | chmod +x /tmp/docker/docker 15 | mv /tmp/docker/docker $INSTALL_DIR/docker 16 | rmdir /tmp/docker/ 17 | rm -rf /tmp/docker.tgz 18 | 19 | echo -e "\n\e[34m»»» 💾 \e[32mInstalled to: \e[33m$(which $CMD)" 20 | echo -e "\e[34m»»» 💡 \e[32mVersion details: \e[39m$($CMD --version)" 21 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/build-only/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian / Ubuntu version: debian-10, debian-9, ubuntu-20.04, ubuntu-18.04 2 | # See https://github.com/microsoft/vscode-dev-containers/tree/master/containers/debian 3 | ARG VARIANT=debian-12 4 | FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT} 5 | 6 | 7 | # Set ENTRYPOINT so that we generate an error if a container is started 8 | ENTRYPOINT [ "exit 1" ] 9 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/build-only/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "build-args", 5 | "dockerFile": "Dockerfile", 6 | "build": { 7 | "args": { 8 | "TEST_ARG": "Hello build-args!" 9 | } 10 | }, 11 | 12 | "mounts": [ 13 | // Keep command history 14 | "source=build-args-bashhistory,target=/home/vscode/commandhistory", 15 | // Mount host docker socket 16 | "type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock" 17 | 18 | ], 19 | 20 | // Set *default* container specific settings.json values on container create. 21 | "settings": { 22 | "terminal.integrated.shell.linux": "/bin/bash", 23 | "files.eol": "\n", 24 | }, 25 | 26 | // Add the IDs of extensions you want installed when the container is created. 27 | // "extensions": [], 28 | 29 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 30 | // "forwardPorts": [], 31 | 32 | // Use 'postCreateCommand' to run commands after the container is created. 33 | // "postCreateCommand": "echo hello", 34 | 35 | "remoteUser": "vscode", 36 | "extensions": [ 37 | "ms-azuretools.vscode-docker", 38 | ] 39 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/config-file/.devcontainer/subfolder/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "config-file", 3 | "image": "mcr.microsoft.com/devcontainers/base:jammy", 4 | "runArgs": [ 5 | "--hostname", "my-host" 6 | ]} -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-non-root/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian / Ubuntu version: debian-10, debian-9, ubuntu-20.04, ubuntu-18.04 2 | # See https://github.com/microsoft/vscode-dev-containers/tree/master/containers/debian 3 | ARG VARIANT=debian-12 4 | FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT} 5 | 6 | 7 | # Avoid warnings by switching to noninteractive 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | 10 | # Set env for tracking that we're running in a devcontainer 11 | ENV DEVCONTAINER=true 12 | 13 | # This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" 14 | # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs 15 | # will be updated to match your local UID/GID (when using the dockerFile property). 16 | # See https://aka.ms/vscode-remote/containers/non-root-user for details. 17 | ARG USERNAME=vscode 18 | ARG USER_UID=1000 19 | ARG USER_GID=$USER_UID 20 | 21 | USER $USERNAME 22 | RUN mkdir -p ~/.local/bin 23 | ENV PATH /home/${USERNAME}/.local/bin:$PATH 24 | 25 | # Configure apt, install packages and general tools 26 | RUN sudo apt-get update \ 27 | && sudo apt-get -y install --no-install-recommends apt-utils dialog nano bash-completion sudo bsdmainutils \ 28 | # 29 | # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed 30 | && sudo apt-get -y install git iproute2 procps lsb-release figlet build-essential 31 | 32 | # Save command line history 33 | RUN echo "export HISTFILE=/home/$USERNAME/commandhistory/.bash_history" >> "/home/$USERNAME/.bashrc" \ 34 | && echo "export PROMPT_COMMAND='history -a'" >> "/home/$USERNAME/.bashrc" \ 35 | && mkdir -p /home/$USERNAME/commandhistory \ 36 | && touch /home/$USERNAME/commandhistory/.bash_history \ 37 | && chown -R $USERNAME /home/$USERNAME/commandhistory 38 | 39 | # Set env for tracking that we're running in a devcontainer 40 | ENV DEVCONTAINER=true 41 | 42 | # golang 43 | COPY scripts/golang.sh /tmp/ 44 | RUN /tmp/golang.sh 45 | 46 | # Set up GOPATH 47 | ENV GOPATH /home/vscode/go 48 | ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 49 | 50 | # docker-client 51 | COPY scripts/docker-client.sh /tmp/ 52 | RUN /tmp/docker-client.sh 53 | 54 | #Add user to docker group 55 | RUN sudo groupadd docker && sudo usermod -aG docker $USERNAME && newgrp docker 56 | 57 | # __DEVCONTAINER_SNIPPET_INSERT__ (control where snippets get inserted using the devcontainer CLI) 58 | 59 | # Switch back to dialog for any ad-hoc use of apt-get 60 | ENV DEBIAN_FRONTEND=dialog 61 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-non-root/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "dc-test", 5 | "dockerFile": "Dockerfile", 6 | "build": { 7 | "cacheFrom": "ghcr.io/devcontainers/ci/tests/docker-from-docker-non-root:latest" 8 | }, 9 | 10 | "mounts": [ 11 | // Keep command history 12 | "source=dc-test-bashhistory,target=/home/vscode/commandhistory", 13 | // Mount host docker socket 14 | "type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock" 15 | 16 | ], 17 | 18 | // Set *default* container specific settings.json values on container create. 19 | "settings": { 20 | "terminal.integrated.shell.linux": "/bin/bash", 21 | "files.eol": "\n", 22 | "go.useLanguageServer": true, 23 | "[go]": { 24 | "editor.snippetSuggestions": "none", 25 | "editor.formatOnSave": true, 26 | "editor.codeActionsOnSave": { 27 | "source.organizeImports": true 28 | } 29 | }, 30 | "gopls": { 31 | "usePlaceholders": true, // add parameter placeholders when completing a function 32 | // Experimental settings 33 | "completeUnimported": true, // autocomplete unimported packages 34 | "deepCompletion": true // enable deep completion 35 | }, 36 | "go.delveConfig": { 37 | "dlvLoadConfig": { 38 | "followPointers": true, 39 | "maxVariableRecurse": 1, 40 | "maxStringLen": 1024, 41 | "maxArrayValues": 64, 42 | "maxStructFields": -1 43 | }, 44 | "apiVersion": 2, 45 | "showGlobalVariables": false, 46 | "debugAdapter": "legacy" 47 | } 48 | }, 49 | 50 | // Add the IDs of extensions you want installed when the container is created. 51 | // "extensions": [], 52 | 53 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 54 | // "forwardPorts": [], 55 | 56 | // Use 'postCreateCommand' to run commands after the container is created. 57 | "postCreateCommand": "./scripts/go-tools.sh", 58 | 59 | "remoteUser": "vscode", 60 | 61 | "extensions": [ 62 | "golang.go", 63 | "stuartleeks.vscode-go-by-example", 64 | "ms-azuretools.vscode-docker", 65 | ] 66 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-non-root/.devcontainer/scripts/docker-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | VERSION=${1:-"20.10.5"} 5 | INSTALL_DIR=${2:-"$HOME/.local/bin"} 6 | CMD=docker 7 | NAME="Docker Client" 8 | 9 | echo -e "\e[34m»»» 📦 \e[32mInstalling \e[33m$NAME \e[35mv$VERSION\e[0m ..." 10 | 11 | mkdir -p $INSTALL_DIR 12 | curl -sSL https://download.docker.com/linux/static/stable/x86_64/docker-$VERSION.tgz -o /tmp/docker.tgz 13 | tar -zxvf /tmp/docker.tgz -C /tmp docker/docker 14 | chmod +x /tmp/docker/docker 15 | mv /tmp/docker/docker $INSTALL_DIR/docker 16 | rmdir /tmp/docker/ 17 | rm -rf /tmp/docker.tgz 18 | 19 | echo -e "\n\e[34m»»» 💾 \e[32mInstalled to: \e[33m$(which $CMD)" 20 | echo -e "\e[34m»»» 💡 \e[32mVersion details: \e[39m$($CMD --version)" 21 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-non-root/.devcontainer/scripts/golang.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | get_latest_release() { 5 | curl --silent "https://go.dev/dl/" | grep -Po -m 1 '(\d+\.\d+\.\d+)\.linux-amd64.tar.gz"' | sed 's/.linux-amd64.tar.gz"//' 6 | } 7 | 8 | VERSION=${1:-"$(get_latest_release)"} 9 | INSTALL_DIR=${2:-"$HOME/.local/bin"} 10 | CMD=go 11 | NAME="Go Language" 12 | 13 | echo -e "\e[34m»»» 📦 \e[32mInstalling \e[33m$NAME \e[35mv$VERSION\e[0m ..." 14 | 15 | cd /tmp 16 | curl -fsSL "https://go.dev/dl/go${VERSION}.linux-amd64.tar.gz" -o golang.tar.gz 17 | sudo tar -xvf golang.tar.gz > /dev/null 18 | sudo rm -rf /usr/local/go 19 | rm -rf /tmp/golang.tar.gz 20 | sudo mv go /usr/local 21 | 22 | echo -e "\n\e[34m»»» 💾 \e[32mInstalled to: \e[33m$(which $CMD)" 23 | echo -e "\e[34m»»» 💡 \e[32mVersion details: \e[39m$($CMD version)" 24 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-non-root/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.3-alpine3.18 as builder 2 | 3 | # Install certs, git, and mercurial 4 | # RUN apk add --no-cache ca-certificates git build-base 5 | 6 | WORKDIR /workspace 7 | 8 | # Copy go.mod etc and download dependencies (leverage docker layer caching) 9 | COPY go.mod go.mod 10 | # COPY go.sum go.sum 11 | ENV GO111MODULE=on 12 | RUN go mod download 13 | 14 | # Copy source code over 15 | COPY ./ . 16 | 17 | # Build 18 | RUN go build -o foo ./ 19 | 20 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 21 | FROM gcr.io/distroless/static:nonroot 22 | WORKDIR / 23 | COPY --from=builder /workspace/foo . 24 | USER nonroot:nonroot 25 | 26 | ENTRYPOINT [ "/foo" ] -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-non-root/Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | @go run main.go 3 | 4 | docker-build: 5 | @docker build . -t foo 6 | 7 | docker-run: docker-build 8 | @docker run --rm foo 9 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-non-root/go.mod: -------------------------------------------------------------------------------- 1 | module example.com/foo 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-non-root/go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devcontainers/ci/26831aa9da924018a5b4e0dee47d60cbe65ffde9/github-tests/Dockerfile/docker-from-docker-non-root/go.sum -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-non-root/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("hello!") 7 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-non-root/scripts/go-tools.sh: -------------------------------------------------------------------------------- 1 | go get github.com/go-delve/delve/cmd/dlv@v1.21.2 2 | # --> Go language server 3 | go get golang.org/x/tools/gopls@v0.14.1 \ 4 | # --> Go symbols and outline for go to symbol support and test support 5 | go get github.com/acroca/go-symbols@v0.1.1 && go get github.com/ramya-rao-a/go-outline@7182a932836a71948db4a81991a494751eccfe77 \ 6 | # --> GolangCI-lint 7 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.43.0 8 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-root/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian / Ubuntu version: debian-10, debian-9, ubuntu-20.04, ubuntu-18.04 2 | # See https://github.com/microsoft/vscode-dev-containers/tree/master/containers/debian 3 | ARG VARIANT=debian-12 4 | FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT} 5 | 6 | 7 | # Avoid warnings by switching to noninteractive 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | 10 | # Set env for tracking that we're running in a devcontainer 11 | ENV DEVCONTAINER=true 12 | 13 | RUN mkdir -p ~/.local/bin 14 | ENV PATH /root/.local/bin:$PATH 15 | 16 | # Configure apt, install packages and general tools 17 | RUN sudo apt-get update \ 18 | && sudo apt-get -y install --no-install-recommends apt-utils dialog nano bash-completion sudo bsdmainutils \ 19 | # 20 | # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed 21 | && sudo apt-get -y install git iproute2 procps lsb-release figlet build-essential 22 | 23 | # Set env for tracking that we're running in a devcontainer 24 | ENV DEVCONTAINER=true 25 | 26 | # golang 27 | COPY scripts/golang.sh /tmp/ 28 | RUN /tmp/golang.sh 29 | 30 | # Set up GOPATH 31 | ENV GOPATH /root/go 32 | ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 33 | 34 | # docker-client 35 | COPY scripts/docker-client.sh /tmp/ 36 | RUN /tmp/docker-client.sh 37 | 38 | # __DEVCONTAINER_SNIPPET_INSERT__ (control where snippets get inserted using the devcontainer CLI) 39 | 40 | # Switch back to dialog for any ad-hoc use of apt-get 41 | ENV DEBIAN_FRONTEND=dialog 42 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-root/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "dc-test", 5 | "dockerFile": "Dockerfile", 6 | "build": { 7 | "cacheFrom": "ghcr.io/devcontainers/ci/tests/docker-from-docker-root:latest" 8 | }, 9 | "mounts": [ 10 | // Mount host docker socket 11 | "type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock" 12 | ], 13 | 14 | // Set *default* container specific settings.json values on container create. 15 | "settings": { 16 | "terminal.integrated.shell.linux": "/bin/bash", 17 | "files.eol": "\n", 18 | "go.useLanguageServer": true, 19 | "[go]": { 20 | "editor.snippetSuggestions": "none", 21 | "editor.formatOnSave": true, 22 | "editor.codeActionsOnSave": { 23 | "source.organizeImports": true 24 | } 25 | }, 26 | "gopls": { 27 | "usePlaceholders": true, // add parameter placeholders when completing a function 28 | // Experimental settings 29 | "completeUnimported": true, // autocomplete unimported packages 30 | "deepCompletion": true // enable deep completion 31 | }, 32 | "go.delveConfig": { 33 | "dlvLoadConfig": { 34 | "followPointers": true, 35 | "maxVariableRecurse": 1, 36 | "maxStringLen": 1024, 37 | "maxArrayValues": 64, 38 | "maxStructFields": -1 39 | }, 40 | "apiVersion": 2, 41 | "showGlobalVariables": false, 42 | "debugAdapter": "legacy" 43 | } 44 | }, 45 | 46 | // Add the IDs of extensions you want installed when the container is created. 47 | // "extensions": [], 48 | 49 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 50 | // "forwardPorts": [], 51 | 52 | // Use 'postCreateCommand' to run commands after the container is created. 53 | "postCreateCommand": "./scripts/go-tools.sh", 54 | 55 | "remoteUser": "root", 56 | 57 | "extensions": [ 58 | "golang.go", 59 | "stuartleeks.vscode-go-by-example", 60 | "ms-azuretools.vscode-docker", 61 | ] 62 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-root/.devcontainer/scripts/docker-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | VERSION=${1:-"20.10.5"} 5 | INSTALL_DIR=${2:-"$HOME/.local/bin"} 6 | CMD=docker 7 | NAME="Docker Client" 8 | 9 | echo -e "\e[34m»»» 📦 \e[32mInstalling \e[33m$NAME \e[35mv$VERSION\e[0m ..." 10 | 11 | mkdir -p $INSTALL_DIR 12 | curl -sSL https://download.docker.com/linux/static/stable/x86_64/docker-$VERSION.tgz -o /tmp/docker.tgz 13 | tar -zxvf /tmp/docker.tgz -C /tmp docker/docker 14 | chmod +x /tmp/docker/docker 15 | mv /tmp/docker/docker $INSTALL_DIR/docker 16 | rmdir /tmp/docker/ 17 | rm -rf /tmp/docker.tgz 18 | 19 | echo -e "\n\e[34m»»» 💾 \e[32mInstalled to: \e[33m$(which $CMD)" 20 | echo -e "\e[34m»»» 💡 \e[32mVersion details: \e[39m$($CMD --version)" 21 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-root/.devcontainer/scripts/golang.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | get_latest_release() { 5 | curl --silent "https://go.dev/dl/" | grep -Po -m 1 '(\d+\.\d+\.\d+)\.linux-amd64.tar.gz"' | sed 's/.linux-amd64.tar.gz"//' 6 | } 7 | 8 | VERSION=${1:-"$(get_latest_release)"} 9 | INSTALL_DIR=${2:-"$HOME/.local/bin"} 10 | CMD=go 11 | NAME="Go Language" 12 | 13 | echo -e "\e[34m»»» 📦 \e[32mInstalling \e[33m$NAME \e[35mv$VERSION\e[0m ..." 14 | 15 | cd /tmp 16 | curl -fsSL "https://go.dev/dl/go${VERSION}.linux-amd64.tar.gz" -o golang.tar.gz 17 | sudo tar -xvf golang.tar.gz > /dev/null 18 | sudo rm -rf /usr/local/go 19 | rm -rf /tmp/golang.tar.gz 20 | sudo mv go /usr/local 21 | 22 | echo -e "\n\e[34m»»» 💾 \e[32mInstalled to: \e[33m$(which $CMD)" 23 | echo -e "\e[34m»»» 💡 \e[32mVersion details: \e[39m$($CMD version)" 24 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-root/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.3-alpine3.18 as builder 2 | 3 | # Install certs, git, and mercurial 4 | # RUN apk add --no-cache ca-certificates git build-base 5 | 6 | WORKDIR /workspace 7 | 8 | # Copy go.mod etc and download dependencies (leverage docker layer caching) 9 | COPY go.mod go.mod 10 | # COPY go.sum go.sum 11 | ENV GO111MODULE=on 12 | RUN go mod download 13 | 14 | # Copy source code over 15 | COPY ./ . 16 | 17 | # Build 18 | RUN go build -o foo ./ 19 | 20 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 21 | FROM gcr.io/distroless/static:nonroot 22 | WORKDIR / 23 | COPY --from=builder /workspace/foo . 24 | USER nonroot:nonroot 25 | 26 | ENTRYPOINT [ "/foo" ] -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-root/Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | @go run main.go 3 | 4 | docker-build: 5 | @docker build . -t foo 6 | 7 | docker-run: docker-build 8 | @docker run --rm foo 9 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-root/go.mod: -------------------------------------------------------------------------------- 1 | module example.com/foo 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-root/go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devcontainers/ci/26831aa9da924018a5b4e0dee47d60cbe65ffde9/github-tests/Dockerfile/docker-from-docker-root/go.sum -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-root/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("hello!") 7 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/docker-from-docker-root/scripts/go-tools.sh: -------------------------------------------------------------------------------- 1 | go get github.com/go-delve/delve/cmd/dlv@v1.21.2 2 | # --> Go language server 3 | go get golang.org/x/tools/gopls@v0.14.1 \ 4 | # --> Go symbols and outline for go to symbol support and test support 5 | go get github.com/acroca/go-symbols@v0.1.1 && go get github.com/ramya-rao-a/go-outline@7182a932836a71948db4a81991a494751eccfe77 \ 6 | # --> GolangCI-lint 7 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.43.0 8 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/dockerfile-context/.devcontainer/CustomDockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian / Ubuntu version: debian-10, debian-9, ubuntu-20.04, ubuntu-18.04 2 | # See https://github.com/microsoft/vscode-dev-containers/tree/master/containers/debian 3 | ARG VARIANT=debian-12 4 | FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT} 5 | 6 | 7 | # Avoid warnings by switching to noninteractive 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | 10 | # Set env for tracking that we're running in a devcontainer 11 | ENV DEVCONTAINER=true 12 | 13 | # This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" 14 | # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs 15 | # will be updated to match your local UID/GID (when using the dockerFile property). 16 | # See https://aka.ms/vscode-remote/containers/non-root-user for details. 17 | ARG USERNAME=vscode 18 | ARG USER_UID=1000 19 | ARG USER_GID=$USER_UID 20 | 21 | USER $USERNAME 22 | RUN \ 23 | mkdir -p ~/.local/bin \ 24 | && echo "export PATH=\$PATH:~/.local/bin" >> ~/.bashrc 25 | 26 | # Configure apt, install packages and general tools 27 | RUN sudo apt-get update \ 28 | && sudo apt-get -y install --no-install-recommends apt-utils dialog nano bash-completion sudo bsdmainutils \ 29 | # 30 | # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed 31 | && sudo apt-get -y install git iproute2 procps lsb-release figlet build-essential 32 | 33 | # Save command line history 34 | RUN echo "export HISTFILE=/home/$USERNAME/commandhistory/.bash_history" >> "/home/$USERNAME/.bashrc" \ 35 | && echo "export PROMPT_COMMAND='history -a'" >> "/home/$USERNAME/.bashrc" \ 36 | && mkdir -p /home/$USERNAME/commandhistory \ 37 | && touch /home/$USERNAME/commandhistory/.bash_history \ 38 | && chown -R $USERNAME /home/$USERNAME/commandhistory 39 | 40 | # Set env for tracking that we're running in a devcontainer 41 | ENV DEVCONTAINER=true 42 | 43 | RUN mkdir -p /tmp 44 | COPY dummy.sh /tmp/ 45 | 46 | # __DEVCONTAINER_SNIPPET_INSERT__ (control where snippets get inserted using the devcontainer CLI) 47 | 48 | # Switch back to dialog for any ad-hoc use of apt-get 49 | ENV DEBIAN_FRONTEND=dialog 50 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/dockerfile-context/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "dockerfile-contxt", 5 | "dockerFile": "CustomDockerfile", 6 | "context" : "../context", 7 | "build": { 8 | "cacheFrom": "ghcr.io/devcontainers/ci/tests/dockerfile-context:latest" 9 | }, 10 | 11 | 12 | // Set *default* container specific settings.json values on container create. 13 | "settings": { 14 | "terminal.integrated.shell.linux": "/bin/bash", 15 | "files.eol": "\n", 16 | }, 17 | 18 | // Add the IDs of extensions you want installed when the container is created. 19 | // "extensions": [], 20 | 21 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 22 | // "forwardPorts": [], 23 | 24 | // Use 'postCreateCommand' to run commands after the container is created. 25 | // "postCreateCommand": "echo hello", 26 | 27 | "remoteUser": "vscode", 28 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/dockerfile-context/context/dummy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Hello from dummy.sh" -------------------------------------------------------------------------------- /github-tests/Dockerfile/env-vars-on-post-create/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian / Ubuntu version: debian-10, debian-9, ubuntu-20.04, ubuntu-18.04 2 | # See https://github.com/microsoft/vscode-dev-containers/tree/master/containers/debian 3 | ARG VARIANT=debian-12 4 | FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT} 5 | 6 | 7 | # Avoid warnings by switching to noninteractive 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | 10 | # Set env for tracking that we're running in a devcontainer 11 | ENV DEVCONTAINER=true 12 | 13 | # This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" 14 | # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs 15 | # will be updated to match your local UID/GID (when using the dockerFile property). 16 | # See https://aka.ms/vscode-remote/containers/non-root-user for details. 17 | ARG USERNAME=vscode 18 | ARG USER_UID=1000 19 | ARG USER_GID=$USER_UID 20 | 21 | USER $USERNAME 22 | RUN \ 23 | mkdir -p ~/.local/bin \ 24 | && echo "export PATH=\$PATH:~/.local/bin" >> ~/.bashrc 25 | 26 | # Configure apt, install packages and general tools 27 | RUN sudo apt-get update \ 28 | && sudo apt-get -y install --no-install-recommends apt-utils dialog nano bash-completion sudo bsdmainutils \ 29 | # 30 | # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed 31 | && sudo apt-get -y install git iproute2 procps lsb-release figlet build-essential 32 | 33 | # Save command line history 34 | RUN echo "export HISTFILE=/home/$USERNAME/commandhistory/.bash_history" >> "/home/$USERNAME/.bashrc" \ 35 | && echo "export PROMPT_COMMAND='history -a'" >> "/home/$USERNAME/.bashrc" \ 36 | && mkdir -p /home/$USERNAME/commandhistory \ 37 | && touch /home/$USERNAME/commandhistory/.bash_history \ 38 | && chown -R $USERNAME /home/$USERNAME/commandhistory 39 | 40 | # Set env for tracking that we're running in a devcontainer 41 | ENV DEVCONTAINER=true 42 | 43 | # __DEVCONTAINER_SNIPPET_INSERT__ (control where snippets get inserted using the devcontainer CLI) 44 | 45 | # Switch back to dialog for any ad-hoc use of apt-get 46 | ENV DEBIAN_FRONTEND=dialog 47 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/env-vars-on-post-create/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "env-vars-on-post-create", 5 | "dockerFile": "Dockerfile", 6 | "build": { 7 | "cacheFrom": "ghcr.io/devcontainers/ci/tests/env-vars-on-post-create:latest" 8 | }, 9 | "remoteEnv": { 10 | "TEST_ENV_VALUE": "${localEnv:TEST_ENV_VALUE}" 11 | }, 12 | 13 | // Add the IDs of extensions you want installed when the container is created. 14 | // "extensions": [], 15 | 16 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 17 | // "forwardPorts": [], 18 | 19 | // Use 'postCreateCommand' to run commands after the container is created. 20 | "postCreateCommand": "./.devcontainer/post-create.sh", 21 | 22 | "remoteUser": "vscode" 23 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/env-vars-on-post-create/.devcontainer/post-create.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | echo "**************************************************" 5 | echo "*** In post-create.sh " 6 | echo "*** TEST_ENV_VALUE=${TEST_ENV_VALUE}" 7 | echo "*** TEST_ENV_VALUE2=${TEST_ENV_VALUE2}" 8 | echo "**************************************************" 9 | echo "post-create: TEST_ENV_VALUE=${TEST_ENV_VALUE}" > marker.txt 10 | echo "post-create: TEST_ENV_VALUE2=${TEST_ENV_VALUE2}" >> marker.txt 11 | 12 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/feature-docker-from-docker/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/go/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.16, 1.17, 1-bullseye, 1.16-bullseye, 1.17-bullseye, 1-buster, 1.16-buster, 1.17-buster 4 | ARG VARIANT="1.18-bullseye" 5 | FROM mcr.microsoft.com/vscode/devcontainers/go:0-${VARIANT} 6 | 7 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 8 | ARG NODE_VERSION="none" 9 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 10 | 11 | # [Optional] Uncomment this section to install additional OS packages. 12 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 13 | # && apt-get -y install --no-install-recommends 14 | 15 | # [Optional] Uncomment the next lines to use go get to install anything else you need 16 | # USER vscode 17 | # RUN go get -x 18 | 19 | # [Optional] Uncomment this line to install global node packages. 20 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 21 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/feature-docker-from-docker/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/go 3 | { 4 | "name": "Go", 5 | "build": { 6 | "dockerfile": "Dockerfile", 7 | "args": { 8 | // Update the VARIANT arg to pick a version of Go: 1, 1.18, 1.17 9 | // Append -bullseye or -buster to pin to an OS version. 10 | // Use -bullseye variants on local arm64/Apple Silicon. 11 | "VARIANT": "1.18-bullseye", 12 | // Options 13 | "NODE_VERSION": "none" 14 | }, 15 | "cacheFrom": "ghcr.io/devcontainers/ci/tests/feature-docker-from-docker:latest" 16 | }, 17 | "runArgs": [ "--cap-add=SYS_PTRACE", "--security-opt", "seccomp=unconfined" ], 18 | 19 | // Set *default* container specific settings.json values on container create. 20 | "settings": { 21 | "go.toolsManagement.checkForUpdates": "local", 22 | "go.useLanguageServer": true, 23 | "go.gopath": "/go" 24 | }, 25 | 26 | // Add the IDs of extensions you want installed when the container is created. 27 | "extensions": [ 28 | "golang.Go" 29 | ], 30 | 31 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 32 | // "forwardPorts": [], 33 | 34 | // Use 'postCreateCommand' to run commands after the container is created. 35 | // "postCreateCommand": "go version", 36 | 37 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 38 | "remoteUser": "vscode", 39 | "features": { 40 | "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {"version": "20.10"} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/feature-docker-from-docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.3-alpine3.18 as builder 2 | 3 | # Install certs, git, and mercurial 4 | # RUN apk add --no-cache ca-certificates git build-base 5 | 6 | WORKDIR /workspace 7 | 8 | # Copy go.mod etc and download dependencies (leverage docker layer caching) 9 | COPY go.mod go.mod 10 | # COPY go.sum go.sum 11 | ENV GO111MODULE=on 12 | RUN go mod download 13 | 14 | # Copy source code over 15 | COPY ./ . 16 | 17 | # Build 18 | RUN go build -o foo ./ 19 | 20 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 21 | FROM gcr.io/distroless/static:nonroot 22 | WORKDIR / 23 | COPY --from=builder /workspace/foo . 24 | USER nonroot:nonroot 25 | 26 | ENTRYPOINT [ "/foo" ] -------------------------------------------------------------------------------- /github-tests/Dockerfile/feature-docker-from-docker/Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | @go run main.go 3 | 4 | docker-build: 5 | @docker build . -t foo 6 | 7 | docker-run: docker-build 8 | @docker run --rm foo 9 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/feature-docker-from-docker/go.mod: -------------------------------------------------------------------------------- 1 | module example.com/foo 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/feature-docker-from-docker/go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devcontainers/ci/26831aa9da924018a5b4e0dee47d60cbe65ffde9/github-tests/Dockerfile/feature-docker-from-docker/go.sum -------------------------------------------------------------------------------- /github-tests/Dockerfile/feature-docker-from-docker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | fmt.Println("hello!") 7 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/image-tag/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian / Ubuntu version: debian-10, debian-9, ubuntu-20.04, ubuntu-18.04 2 | # See https://github.com/microsoft/vscode-dev-containers/tree/master/containers/debian 3 | ARG VARIANT=debian-12 4 | FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT} 5 | 6 | 7 | # Avoid warnings by switching to noninteractive 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | 10 | # Set env for tracking that we're running in a devcontainer 11 | ENV DEVCONTAINER=true 12 | 13 | 14 | 15 | # This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" 16 | # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs 17 | # will be updated to match your local UID/GID (when using the dockerFile property). 18 | # See https://aka.ms/vscode-remote/containers/non-root-user for details. 19 | ARG USERNAME=vscode 20 | ARG USER_UID=1000 21 | ARG USER_GID=$USER_UID 22 | 23 | USER $USERNAME 24 | RUN mkdir -p ~/.local/bin 25 | ENV PATH /home/${USERNAME}/.local/bin:$PATH 26 | 27 | # Configure apt, install packages and general tools 28 | RUN sudo apt-get update \ 29 | && sudo apt-get -y install --no-install-recommends apt-utils dialog nano bash-completion sudo bsdmainutils \ 30 | # 31 | # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed 32 | && sudo apt-get -y install git iproute2 procps lsb-release figlet build-essential 33 | 34 | # Save command line history 35 | RUN echo "export HISTFILE=/home/$USERNAME/commandhistory/.bash_history" >> "/home/$USERNAME/.bashrc" \ 36 | && echo "export PROMPT_COMMAND='history -a'" >> "/home/$USERNAME/.bashrc" \ 37 | && mkdir -p /home/$USERNAME/commandhistory \ 38 | && touch /home/$USERNAME/commandhistory/.bash_history \ 39 | && chown -R $USERNAME /home/$USERNAME/commandhistory 40 | 41 | # Set env for tracking that we're running in a devcontainer 42 | ENV DEVCONTAINER=true 43 | 44 | # docker-client 45 | COPY scripts/docker-client.sh /tmp/ 46 | RUN /tmp/docker-client.sh 47 | 48 | #Add user to docker group 49 | RUN sudo groupadd docker && sudo usermod -aG docker $USERNAME && newgrp docker 50 | 51 | # __DEVCONTAINER_SNIPPET_INSERT__ (control where snippets get inserted using the devcontainer CLI) 52 | 53 | # Switch back to dialog for any ad-hoc use of apt-get 54 | ENV DEBIAN_FRONTEND=dialog 55 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/image-tag/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "image-tag", 5 | "dockerFile": "Dockerfile", 6 | 7 | "mounts": [ 8 | // Keep command history 9 | "source=build-args-bashhistory,target=/home/vscode/commandhistory", 10 | // Mount host docker socket 11 | "type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock" 12 | ], 13 | 14 | // Set *default* container specific settings.json values on container create. 15 | "settings": { 16 | "terminal.integrated.shell.linux": "/bin/bash", 17 | "files.eol": "\n", 18 | }, 19 | 20 | // Add the IDs of extensions you want installed when the container is created. 21 | // "extensions": [], 22 | 23 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 24 | // "forwardPorts": [], 25 | 26 | // Use 'postCreateCommand' to run commands after the container is created. 27 | // "postCreateCommand": "echo hello", 28 | 29 | "remoteUser": "vscode", 30 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/image-tag/.devcontainer/scripts/docker-client.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | VERSION=${1:-"20.10.5"} 5 | INSTALL_DIR=${2:-"$HOME/.local/bin"} 6 | CMD=docker 7 | NAME="Docker Client" 8 | 9 | echo -e "\e[34m»»» 📦 \e[32mInstalling \e[33m$NAME \e[35mv$VERSION\e[0m ..." 10 | 11 | mkdir -p $INSTALL_DIR 12 | curl -sSL https://download.docker.com/linux/static/stable/x86_64/docker-$VERSION.tgz -o /tmp/docker.tgz 13 | tar -zxvf /tmp/docker.tgz -C /tmp docker/docker 14 | chmod +x /tmp/docker/docker 15 | mv /tmp/docker/docker $INSTALL_DIR/docker 16 | rmdir /tmp/docker/ 17 | rm -rf /tmp/docker.tgz 18 | 19 | echo -e "\n\e[34m»»» 💾 \e[32mInstalled to: \e[33m$(which $CMD)" 20 | echo -e "\e[34m»»» 💡 \e[32mVersion details: \e[39m$($CMD --version)" 21 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/outputs/.devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "outputs", 5 | "image": "mcr.microsoft.com/vscode/devcontainers/base:debian-12", 6 | "build": { 7 | "cacheFrom": "ghcr.io/devcontainers/ci/tests/outputs:latest" 8 | }, 9 | 10 | // Add the IDs of extensions you want installed when the container is created. 11 | // "extensions": [], 12 | 13 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 14 | // "forwardPorts": [], 15 | 16 | // Use 'postCreateCommand' to run commands after the container is created. 17 | // "postCreateCommand": "echo hello", 18 | 19 | "remoteUser": "vscode" 20 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/platform-with-runcmd/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian / Ubuntu version: debian-10, debian-9, ubuntu-20.04, ubuntu-18.04 2 | # See https://github.com/microsoft/vscode-dev-containers/tree/master/containers/debian 3 | ARG VARIANT=debian-12 4 | FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT} 5 | 6 | 7 | # Avoid warnings by switching to noninteractive 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | 10 | # Set env for tracking that we're running in a devcontainer 11 | ENV DEVCONTAINER=true 12 | 13 | USER root 14 | 15 | # Configure apt, install packages and general tools 16 | RUN sudo apt-get update \ 17 | && sudo apt-get -y install --no-install-recommends apt-utils dialog nano bash-completion sudo bsdmainutils \ 18 | # 19 | # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed 20 | && sudo apt-get -y install git iproute2 procps lsb-release figlet build-essential 21 | 22 | 23 | # This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" 24 | # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs 25 | # will be updated to match your local UID/GID (when using the dockerFile property). 26 | # See https://aka.ms/vscode-remote/containers/non-root-user for details. 27 | ARG USERNAME=vscode 28 | ARG USER_UID=1000 29 | ARG USER_GID=$USER_UID 30 | 31 | USER $USERNAME 32 | RUN \ 33 | mkdir -p ~/.local/bin \ 34 | && echo "export PATH=\$PATH:~/.local/bin" >> ~/.bashrc 35 | 36 | 37 | # Save command line history 38 | RUN echo "export HISTFILE=/home/$USERNAME/commandhistory/.bash_history" >> "/home/$USERNAME/.bashrc" \ 39 | && echo "export PROMPT_COMMAND='history -a'" >> "/home/$USERNAME/.bashrc" \ 40 | && mkdir -p /home/$USERNAME/commandhistory \ 41 | && touch /home/$USERNAME/commandhistory/.bash_history \ 42 | && chown -R $USERNAME /home/$USERNAME/commandhistory 43 | 44 | # Set env for tracking that we're running in a devcontainer 45 | ENV DEVCONTAINER=true 46 | 47 | # __DEVCONTAINER_SNIPPET_INSERT__ (control where snippets get inserted using the devcontainer CLI) 48 | 49 | # Switch back to dialog for any ad-hoc use of apt-get 50 | ENV DEBIAN_FRONTEND=dialog 51 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/platform-with-runcmd/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "run-args", 5 | "dockerFile": "Dockerfile", 6 | "build": { 7 | "cacheFrom": "ghcr.io/devcontainers/ci/tests/platform-with-runcmd:latest" 8 | }, 9 | 10 | 11 | "runArgs": [ 12 | "--hostname", "my-host" 13 | ], 14 | 15 | "mounts": [ 16 | // Keep command history 17 | "source=build-args-bashhistory,target=/home/vscode/commandhistory", 18 | 19 | ], 20 | 21 | // Set *default* container specific settings.json values on container create. 22 | "settings": { 23 | "terminal.integrated.shell.linux": "/bin/bash", 24 | "files.eol": "\n", 25 | }, 26 | 27 | // Add the IDs of extensions you want installed when the container is created. 28 | // "extensions": [], 29 | 30 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 31 | // "forwardPorts": [], 32 | 33 | // Use 'postCreateCommand' to run commands after the container is created. 34 | // "postCreateCommand": "echo hello", 35 | 36 | "remoteUser": "vscode", 37 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/run-args/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian / Ubuntu version: debian-10, debian-9, ubuntu-20.04, ubuntu-18.04 2 | # See https://github.com/microsoft/vscode-dev-containers/tree/master/containers/debian 3 | ARG VARIANT=debian-12 4 | FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT} 5 | 6 | 7 | # Avoid warnings by switching to noninteractive 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | 10 | # Set env for tracking that we're running in a devcontainer 11 | ENV DEVCONTAINER=true 12 | 13 | # This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" 14 | # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs 15 | # will be updated to match your local UID/GID (when using the dockerFile property). 16 | # See https://aka.ms/vscode-remote/containers/non-root-user for details. 17 | ARG USERNAME=vscode 18 | ARG USER_UID=1000 19 | ARG USER_GID=$USER_UID 20 | 21 | USER $USERNAME 22 | RUN \ 23 | mkdir -p ~/.local/bin \ 24 | && echo "export PATH=\$PATH:~/.local/bin" >> ~/.bashrc 25 | 26 | # Configure apt, install packages and general tools 27 | RUN sudo apt-get update \ 28 | && sudo apt-get -y install --no-install-recommends apt-utils dialog nano bash-completion sudo bsdmainutils \ 29 | # 30 | # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed 31 | && sudo apt-get -y install git iproute2 procps lsb-release figlet build-essential 32 | 33 | # Save command line history 34 | RUN echo "export HISTFILE=/home/$USERNAME/commandhistory/.bash_history" >> "/home/$USERNAME/.bashrc" \ 35 | && echo "export PROMPT_COMMAND='history -a'" >> "/home/$USERNAME/.bashrc" \ 36 | && mkdir -p /home/$USERNAME/commandhistory \ 37 | && touch /home/$USERNAME/commandhistory/.bash_history \ 38 | && chown -R $USERNAME /home/$USERNAME/commandhistory 39 | 40 | # Set env for tracking that we're running in a devcontainer 41 | ENV DEVCONTAINER=true 42 | 43 | # __DEVCONTAINER_SNIPPET_INSERT__ (control where snippets get inserted using the devcontainer CLI) 44 | 45 | # Switch back to dialog for any ad-hoc use of apt-get 46 | ENV DEBIAN_FRONTEND=dialog 47 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/run-args/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "run-args", 5 | "dockerFile": "Dockerfile", 6 | "build": { 7 | "cacheFrom": "ghcr.io/devcontainers/ci/tests/run-args:latest" 8 | }, 9 | 10 | 11 | "runArgs": [ 12 | "--hostname", "my-host" 13 | ], 14 | 15 | "mounts": [ 16 | // Keep command history 17 | "source=build-args-bashhistory,target=/home/vscode/commandhistory", 18 | 19 | ], 20 | 21 | // Set *default* container specific settings.json values on container create. 22 | "settings": { 23 | "terminal.integrated.shell.linux": "/bin/bash", 24 | "files.eol": "\n", 25 | }, 26 | 27 | // Add the IDs of extensions you want installed when the container is created. 28 | // "extensions": [], 29 | 30 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 31 | // "forwardPorts": [], 32 | 33 | // Use 'postCreateCommand' to run commands after the container is created. 34 | // "postCreateCommand": "echo hello", 35 | 36 | "remoteUser": "vscode", 37 | } -------------------------------------------------------------------------------- /github-tests/Dockerfile/skip-user-update/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Debian / Ubuntu version: debian-10, debian-9, ubuntu-20.04, ubuntu-18.04 2 | # See https://github.com/microsoft/vscode-dev-containers/tree/master/containers/debian 3 | ARG VARIANT=debian-12 4 | FROM mcr.microsoft.com/vscode/devcontainers/base:${VARIANT} 5 | 6 | 7 | # Avoid warnings by switching to noninteractive 8 | ENV DEBIAN_FRONTEND=noninteractive 9 | 10 | # Set env for tracking that we're running in a devcontainer 11 | ENV DEVCONTAINER=true 12 | 13 | 14 | 15 | # This Dockerfile adds a non-root user with sudo access. Use the "remoteUser" 16 | # property in devcontainer.json to use it. On Linux, the container user's GID/UIDs 17 | # will be updated to match your local UID/GID (when using the dockerFile property). 18 | # See https://aka.ms/vscode-remote/containers/non-root-user for details. 19 | ARG USERNAME=vscode 20 | ARG USER_UID=1000 21 | ARG USER_GID=$USER_UID 22 | 23 | USER $USERNAME 24 | RUN mkdir -p ~/.local/bin 25 | ENV PATH /home/${USERNAME}/.local/bin:$PATH 26 | 27 | # Configure apt, install packages and general tools 28 | RUN sudo apt-get update \ 29 | && sudo apt-get -y install --no-install-recommends apt-utils dialog nano bash-completion sudo bsdmainutils \ 30 | # 31 | # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed 32 | && sudo apt-get -y install git iproute2 procps lsb-release figlet build-essential 33 | 34 | # Save command line history 35 | RUN echo "export HISTFILE=/home/$USERNAME/commandhistory/.bash_history" >> "/home/$USERNAME/.bashrc" \ 36 | && echo "export PROMPT_COMMAND='history -a'" >> "/home/$USERNAME/.bashrc" \ 37 | && mkdir -p /home/$USERNAME/commandhistory \ 38 | && touch /home/$USERNAME/commandhistory/.bash_history \ 39 | && chown -R $USERNAME /home/$USERNAME/commandhistory 40 | 41 | # Set env for tracking that we're running in a devcontainer 42 | ENV DEVCONTAINER=true 43 | 44 | # __DEVCONTAINER_SNIPPET_INSERT__ (control where snippets get inserted using the devcontainer CLI) 45 | 46 | # Switch back to dialog for any ad-hoc use of apt-get 47 | ENV DEBIAN_FRONTEND=dialog 48 | -------------------------------------------------------------------------------- /github-tests/Dockerfile/skip-user-update/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.117.1/containers/go 3 | { 4 | "name": "skip-uesr-update", 5 | "dockerFile": "Dockerfile", 6 | "build": { 7 | "cacheFrom": "ghcr.io/devcontainers/ci/tests/skip-user-update:latest" 8 | }, 9 | 10 | "mounts": [ 11 | // Keep command history 12 | "source=build-args-bashhistory,target=/home/vscode/commandhistory", 13 | ], 14 | 15 | // Set *default* container specific settings.json values on container create. 16 | "settings": { 17 | "terminal.integrated.shell.linux": "/bin/bash", 18 | "files.eol": "\n", 19 | }, 20 | 21 | // Add the IDs of extensions you want installed when the container is created. 22 | // "extensions": [], 23 | 24 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 25 | // "forwardPorts": [], 26 | 27 | // Use 'postCreateCommand' to run commands after the container is created. 28 | // "postCreateCommand": "echo hello", 29 | 30 | "remoteUser": "vscode", 31 | } -------------------------------------------------------------------------------- /github-tests/docker-compose/features/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster 2 | ARG VARIANT=20-bullseye 3 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} 4 | 5 | # Install MongoDB command line tools if on buster and x86_64 (arm64 not supported) 6 | ARG MONGO_TOOLS_VERSION=5.0 7 | RUN . /etc/os-release \ 8 | && if [ "${VERSION_CODENAME}" = "buster" ] && [ "$(dpkg --print-architecture)" = "amd64" ]; then \ 9 | curl -sSL "https://www.mongodb.org/static/pgp/server-${MONGO_TOOLS_VERSION}.asc" | gpg --dearmor > /usr/share/keyrings/mongodb-archive-keyring.gpg \ 10 | && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/mongodb-archive-keyring.gpg] http://repo.mongodb.org/apt/debian $(lsb_release -cs)/mongodb-org/${MONGO_TOOLS_VERSION} main" | tee /etc/apt/sources.list.d/mongodb-org-${MONGO_TOOLS_VERSION}.list \ 11 | && apt-get update && export DEBIAN_FRONTEND=noninteractive \ 12 | && apt-get install -y mongodb-database-tools mongodb-mongosh \ 13 | && apt-get clean -y && rm -rf /var/lib/apt/lists/*; \ 14 | fi 15 | 16 | # [Optional] Uncomment this section to install additional OS packages. 17 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 18 | # && apt-get -y install --no-install-recommends 19 | 20 | # [Optional] Uncomment if you want to install an additional version of node using nvm 21 | # ARG EXTRA_NODE_VERSION=10 22 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 23 | 24 | # [Optional] Uncomment if you want to install more global node modules 25 | # RUN su node -c "npm install -g " 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /github-tests/docker-compose/features/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | // https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/javascript-node-mongo 3 | // Update the VARIANT arg in docker-compose.yml to pick a Node.js version 4 | { 5 | "name": "Node.js & Mongo DB", 6 | "dockerComposeFile": "docker-compose.yml", 7 | "service": "app", 8 | "workspaceFolder": "/workspace", 9 | 10 | // Set *default* container specific settings.json values on container create. 11 | "settings": {}, 12 | 13 | // Add the IDs of extensions you want installed when the container is created. 14 | "extensions": [ 15 | "dbaeumer.vscode-eslint", 16 | "mongodb.mongodb-vscode" 17 | ], 18 | 19 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 20 | // "forwardPorts": [3000, 27017], 21 | 22 | // Use 'postCreateCommand' to run commands after the container is created. 23 | // "postCreateCommand": "yarn install", 24 | 25 | // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 26 | "remoteUser": "node", 27 | "features": { 28 | "ghcr.io/devcontainers/features/go:1": {"version":"1.18"} 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /github-tests/docker-compose/features/.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | args: 9 | # Update 'VARIANT' to pick an LTS version of Node.js: 18, 16, 14. 10 | # Append -bullseye or -buster to pin to an OS version. 11 | # Use -bullseye variants on local arm64/Apple Silicon. 12 | VARIANT: 20-bullseye 13 | volumes: 14 | - ..:/workspace:cached 15 | 16 | # Overrides default command so things don't shut down after the process ends. 17 | command: sleep infinity 18 | 19 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function. 20 | network_mode: service:db 21 | # Uncomment the next line to use a non-root user for all processes. 22 | # user: node 23 | 24 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally. 25 | # (Adding the "ports" property to this file will not forward from a Codespace.) 26 | 27 | db: 28 | image: mongo:latest 29 | restart: unless-stopped 30 | volumes: 31 | - mongodb-data:/data/db 32 | # Uncomment to change startup options 33 | # environment: 34 | # MONGO_INITDB_ROOT_USERNAME: root 35 | # MONGO_INITDB_ROOT_PASSWORD: example 36 | # MONGO_INITDB_DATABASE: your-database-here 37 | 38 | # Add "forwardPorts": ["27017"] to **devcontainer.json** to forward MongoDB locally. 39 | # (Adding the "ports" property to this file will not forward from a Codespace.) 40 | 41 | volumes: 42 | mongodb-data: null 43 | -------------------------------------------------------------------------------- /maintainers.md: -------------------------------------------------------------------------------- 1 | # Maintainers 2 | 3 | This document is targetted at maintainers of the project. 4 | 5 | ## PR Comment bot commands 6 | 7 | **Notes** 8 | - these commands are not immediate - you need to wait for the GitHub action that performs the task to start up. 9 | - builds triggered via these commands will use the workflow definitions from `main`. To test workflow changes before merging to `main`, push the changes to a branch in the repo and use the `ci_branch` workflow. 10 | 11 | These commands can only be run when commented by a user who is identified as a repo collaborator (see [granting access to run commands](#granting-access-to-run-commands)) 12 | 13 | ### `/help` 14 | 15 | This command will cause the pr-comment-bot to respond with a comment listing the available commands. 16 | 17 | ### `/test []` 18 | 19 | This command runs the build, deploy, and smoke tests for a PR. 20 | 21 | For PRs from maintainers (i.e. users with write access to microsoft/AzureTRE), `/test` is sufficient. 22 | 23 | For other PRs, the checks below should be carried out. Once satisfied that the PR is safe to run tests against, you should use `/test ` where `` is the SHA for the commit that you have verified. 24 | You can use the full or short form of the SHA, but it must be at least 7 characters (GitHub UI shows 7 characters). 25 | 26 | **IMPORTANT** 27 | 28 | This command works on PRs from forks, and makes the deployment secrets available. 29 | Before running tests on a PR, ensure that there are no changes in the PR that could have unintended consequences (e.g. leak secrets or perform undesirable operations in the testing subscription). 30 | 31 | Check for changes to anything that is run during the build/deploy/test cycle, including: 32 | - modifications to workflows (including adding new actions or changing versions of existing actions) 33 | - modifications to scripts 34 | - new packages being installed 35 | 36 | ### `/test-force-approve` 37 | 38 | This command skips running tests for a build and marks the checks as completed. 39 | This is intended to be used in scenarios where running the tests for a PR doesn't add value (for example, changing a workflow file that is always pulled from the default branch). 40 | 41 | ## Granting access to run commands 42 | 43 | Currently, the GitHub API to determine whether a user is a collaborator doesn't seem to respect permissions that a user is granted via a group. As a result, users need to be directly granted `write` permission in the repo to be able to run the comment bot commands. 44 | 45 | ## Implementation notes 46 | 47 | The pr-bot workflow is in `.github/workflows/pr-bot.yml`. Most of the logic for handling commands is split out into `.github/scripts/build.js` and there are accompanying tests in the same folder (`yarn install && yarn test` to run tests). 48 | 49 | The `build.js` script parses the comment text and sets various output values that are then used to control the behaviour of the remaining workflow. The core of the workflow is in `ci_common.yml` and is re-used between the pr-bot and `ci_main.yml` (triggered for merges into `main` to make a release). 50 | -------------------------------------------------------------------------------- /scripts/build-local.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 5 | 6 | SKIP_VSIX=1 "$script_dir/build-test-package.sh" 7 | -------------------------------------------------------------------------------- /scripts/build-test-package.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 5 | 6 | sudo chown -R $(whoami) ~ # TODO - remove this 7 | 8 | figlet common 9 | cd "$script_dir/../common" 10 | npm install 11 | npm run build 12 | npm run test 13 | 14 | figlet GH Action 15 | cd "$script_dir/../github-action" 16 | npm install 17 | npm run all 18 | 19 | figlet AzDO Task 20 | cd "$script_dir/../azdo-task/DevcontainersCi" 21 | cp "$script_dir/../docs/azure-devops-task.md" "$script_dir/../azdo-task/README.md" 22 | cp "$script_dir/../LICENSE" "$script_dir/../azdo-task/LICENSE.md" 23 | npm install 24 | npm run all 25 | cd "$script_dir/../azdo-task" 26 | 27 | if [[ -n "$SKIP_VSIX" ]]; then 28 | echo "SKIP_VSIX set - skipping VSIX creation" 29 | else 30 | echo "Creating VSIX (BUILD_NUMBER=${BUILD_NUMBER})" 31 | ./scripts/build-package.sh --set-patch-version $BUILD_NUMBER 32 | 33 | echo "Copying VSIX files to output folder" 34 | mkdir -p "$script_dir/../output" 35 | cp *.vsix "$script_dir/../output/" 36 | ls -l "$script_dir/../output/" 37 | fi 38 | 39 | if [[ -z $IS_CI ]]; then 40 | echo "IS_CI not set, skipping git status check" 41 | exit 0 42 | fi 43 | 44 | figlet git status 45 | cd "$script_dir/.." 46 | # vss-extension.json and task.json have their version info modified by the build 47 | # reset these before checking for changes 48 | git checkout azdo-task/vss-extension.json 49 | git checkout azdo-task/DevcontainersCi/task.json 50 | # The GH action to generate the build number leaves a BUILD_NUMBER file behind 51 | if [[ -f BUILD_NUMBER ]]; then 52 | rm BUILD_NUMBER 53 | fi 54 | if [[ -n $(git status --short) ]]; then 55 | echo "*** There are unexpected changes in the working directory (see git status output below)" 56 | echo "*** Ensure you have run scripts/build-local.sh" 57 | git status 58 | exit 1 59 | fi 60 | -------------------------------------------------------------------------------- /scripts/get-latest-cli-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 5 | 6 | # Get run id for latest successful CI build on main 7 | echo "Finding latest CI run ..." 8 | run_id=$(gh run list --repo devcontainers/cli --workflow "Dev Containers CI" --branch main --json databaseId,conclusion | jq -r 'map(select(.conclusion=="success")) | first | .databaseId') 9 | 10 | # create tmp dir for downloading artifacts 11 | tmp_dir="$script_dir/tmp" 12 | rm -rf "$tmp_dir" 13 | mkdir -p "$tmp_dir" 14 | echo '*' > "$tmp_dir/.gitignore" 15 | 16 | echo "Got run id $run_id, downloading artifacts ..." 17 | gh run download --repo devcontainers/cli $run_id --dir "$tmp_dir" 18 | 19 | cli_tar_folder=$(ls $tmp_dir | grep dev-containers-cli-.*.tgz) 20 | if [[ -z "$cli_tar_folder" ]]; then 21 | echo "Could not find cli tar folder in downloaded artifacts" 22 | exit 1 23 | fi 24 | 25 | cli_tar_file=$(ls $tmp_dir/$cli_tar_folder | grep dev-containers-cli-.*.tgz) 26 | if [[ -z "$cli_tar_file" ]]; then 27 | echo "Could not find cli tar file in $cli_tar_folder" 28 | exit 1 29 | fi 30 | echo "Got cli tar file $cli_tar_file ..." 31 | 32 | 33 | echo "Extracting cli ..." 34 | mkdir -p "$tmp_dir/cli" 35 | tar xf "$tmp_dir/$cli_tar_folder/$cli_tar_file" --directory="$tmp_dir/cli" 36 | 37 | # Clean cli folder 38 | cli_dir="$script_dir/../cli" 39 | rm -rf "$cli_dir" 40 | mkdir -p "$cli_dir" 41 | 42 | mv "$tmp_dir/cli/package/"* "$cli_dir" 43 | -------------------------------------------------------------------------------- /scripts/gh-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Create a temporary branch 5 | git checkout -B release-tmp 6 | 7 | # Leave just the root-level files/folders needed for the release 8 | find -maxdepth 1 \ 9 | -not -name '.' \ 10 | -not -name '..' \ 11 | -not -name '.git' \ 12 | -not -name action.yml \ 13 | -not -name github-action \ 14 | | xargs rm -rf 15 | 16 | # Leave just the dist folder in github-action 17 | (cd github-action && find -maxdepth 1 \ 18 | -not -name '.' \ 19 | -not -name '..' \ 20 | -not -name 'dist' \ 21 | -not -name 'run-main.js' \ 22 | -not -name 'run-post.js' \ 23 | | xargs rm -rf ) 24 | 25 | 26 | # Create a commit 27 | git add . 28 | git commit -m "release" 29 | 30 | # Create and push tag for full version number for the release 31 | git tag "$RELEASE_NAME" -f 32 | git push origin "$RELEASE_NAME" -f 33 | 34 | # Create a release for the current git ref using the full version number 35 | gh release create "$RELEASE_NAME" \ 36 | --title "Release $RELEASE_NAME" \ 37 | --generate-notes 38 | 39 | # Update and push short version number 40 | git tag "$TAG_NAME" -f 41 | git push origin "$TAG_NAME" -f 42 | -------------------------------------------------------------------------------- /scripts/publish-azdo-task.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 5 | 6 | if [[ -z "$AZDO_TOKEN" ]]; then 7 | echo "AZDO_TOKEN must be specified" 8 | exit 1 9 | fi 10 | 11 | cd "$script_dir/.." 12 | echo "Publishing extension..." 13 | vsix_file=$(ls output/devcontainers.*.vsix) 14 | echo "Using VSIX_FILE=$vsix_file" 15 | tfx extension publish --token $AZDO_TOKEN --vsix $vsix_file --override "{\"public\": true}" 16 | -------------------------------------------------------------------------------- /scripts/test-azdo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" 5 | 6 | if [[ -z "$AZDO_TOKEN" ]]; then 7 | echo "AZDO_TOKEN must be specified" 8 | exit 1 9 | fi 10 | 11 | cd "$script_dir/.." 12 | vsix_file=$(ls output/devcontainers-dev.*.vsix) 13 | echo "Using VSIX_FILE=$vsix_file" 14 | 15 | # Publish as non-public and as devcontainers-dev 16 | echo "Publishing private extension version..." 17 | tfx extension publish --token "$AZDO_TOKEN" --vsix "$vsix_file" --override "{\"public\": false, \"publisher\": \"devcontainers-dev\"}" --share-with monacotools 18 | 19 | echo "Uninstalling private extension" 20 | AZURE_DEVOPS_EXT_PAT="$AZDO_TOKEN" az devops extension uninstall --organization "$AZDO_ORG" --extension-id "ci" --publisher-id "devcontainers-dev" --yes --verbose || true 21 | 22 | echo "Installing private extension" 23 | tfx extension install --token "$AZDO_TOKEN" --vsix "$vsix_file" --service-url "$AZDO_ORG" 24 | 25 | sleep 30s # hacky workaround for AzDO picking up stale extension version 26 | 27 | echo "About to start AzDo build" 28 | commit=$(git rev-parse HEAD) 29 | echo " commit : $commit" 30 | echo " image_tag: $IMAGE_TAG" 31 | "$script_dir/../azdo-task/scripts/run-azdo-build.sh" --organization "$AZDO_ORG" --project "$AZDO_PROJECT" --build "$AZDO_BUILD" --image-tag "$IMAGE_TAG" --commit "$commit" 32 | --------------------------------------------------------------------------------