Skip to content

Commit

Permalink
Add support for multiple tags (devcontainers#183)
Browse files Browse the repository at this point in the history
* Add multiple tag support for Action

* Add multiple --tag options to build

* Block automatic cache from when multiple image tags are used

* Move else to the right if

* Update devcontainer build call

* Add logging for debug

* Remove logging

* Add tag to imageSource for skopeo copy

* Update AzDO task

* Fix variables

* Update readmes

* Add tests for multiple tags

---------

Co-authored-by: Stuart Leeks <[email protected]>
  • Loading branch information
natescherer and stuartleeks authored Feb 7, 2023
1 parent 511b677 commit c5e74b5
Show file tree
Hide file tree
Showing 19 changed files with 301 additions and 120 deletions.
89 changes: 89 additions & 0 deletions .github/workflows/ci_common.yml
Original file line number Diff line number Diff line change
Expand Up @@ -802,3 +802,92 @@ jobs:
fi
env:
runCmdOutput: ${{ steps.platform-with-runcmd.outputs.runCmdOutput }}

test-multiple-tags-job1:
# This is split into multiple parts because pushing the image happens during the post-job phase
# and thus testing if the tags exist can't happen as part of job 1
name: Test multiple tags job 1
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout
uses: actions/checkout@v2
with:
persist-credentials: false
# if the following value is missing (i.e. not triggered via comment workflow)
# then the default checkout will apply
ref: ${{ inputs.prRef }}

- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push with multiple tags and single platform
uses: ./
id: multiple-tags-single-platform
with:
subFolder: github-tests/Dockerfile/build-only
imageName: ghcr.io/devcontainers/ci/tests/multiple-tags-single-platform
imageTag: tag1-${{ github.run_id }},tag2-${{ github.run_id }}
push: always

- name: Set up QEMU for multi-architecture builds
uses: docker/setup-qemu-action@v1

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1

- name: Install updated Skopeo
# This can be omitted once runner images have a version of Skopeo > 1.4.1
# See https://github.com/containers/skopeo/issues/1874
run: |
sudo apt purge buildah golang-github-containers-common podman skopeo
sudo apt autoremove --purge
REPO_URL="https://download.opensuse.org/repositories/devel:/kubic:/libcontainers:/unstable"
source /etc/os-release
sudo sh -c "echo 'deb ${REPO_URL}/x${NAME}_${VERSION_ID}/ /' > /etc/apt/sources.list.d/devel:kubic:libcontainers:unstable.list"
sudo wget -qnv https://download.opensuse.org/repositories/devel:kubic:libcontainers:stable/x${NAME}_${VERSION_ID}/Release.key -O Release.key
sudo apt-key add Release.key
sudo apt-get update
sudo apt-get install skopeo
- name: Build and push with multiple tags and multiple platforms
uses: ./
id: multiple-tags-multiple-platforms
with:
subFolder: github-tests/Dockerfile/platform-with-runcmd
imageName: ghcr.io/devcontainers/ci/tests/multiple-tags-multiple-platforms
imageTag: tag1-${{ github.run_id }},tag2-${{ github.run_id }}
platform: linux/amd64,linux/arm64
push: always

test-multiple-tags-job2:
name: Test multiple tags job 2
runs-on: ubuntu-latest
needs: [build, test-multiple-tags-job1]
steps:
- name: Validate multiple tags single platform
run: |
set -e
echo "Querying GitHub API for versions of package devcontainers-ci/tests/multiple-tags-single-platform..."
versions=$(curl -s -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/users/devcontainers/packages/container/ci%2Ftests%2Fmultiple-tags-single-platform/versions)
echo "Checking tag1-${{ github.run_id }} exists... "
echo "$versions" | grep -q "tag1-${{ github.run_id }}"
echo "Tag exists."
echo "Checking tag2-${{ github.run_id }} exists... "
echo "$versions" | grep -q "tag1-${{ github.run_id }}"
echo "Tag exists."
- name: Validate multiple tags multiple platforms
run: |
set -e
echo "Querying GitHub API for versions of package devcontainers-ci/tests/multiple-tags-multiple-platforms..."
versions=$(curl -s -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/users/devcontainers/packages/container/ci%2Ftests%2Fmultiple-tags-multiple-platforms/versions)
echo "Checking tag1-${{ github.run_id }} exists... "
echo "$versions" | grep -q "tag1-${{ github.run_id }}"
echo "Tag exists."
echo "Checking tag2-${{ github.run_id }} exists... "
echo "$versions" | grep -q "tag1-${{ github.run_id }}"
echo "Tag exists."
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ inputs:
description: Image name (including registry)
imageTag:
required: false
description: Image tag (defaults to latest)
description: One or more comma-separated image tags (defaults to latest)
platform:
require: false
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.
Expand Down
2 changes: 1 addition & 1 deletion azdo-task/DevcontainersCi/dist/dev-container-cli.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface DevContainerCliBuildResult extends DevContainerCliSuccessResult
}
export interface DevContainerCliBuildArgs {
workspaceFolder: string;
imageName?: string;
imageName?: string[];
platform?: string;
additionalCacheFroms?: string[];
userDataFolder?: string;
Expand Down
50 changes: 32 additions & 18 deletions azdo-task/DevcontainersCi/dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,24 +268,33 @@ function runMain() {
const buildxOutput = platform ? 'type=oci,dest=/tmp/output.tar' : undefined;
const log = (message) => console.log(message);
const workspaceFolder = path_1.default.resolve(checkoutPath, subFolder);
const fullImageName = imageName
? `${imageName}:${imageTag !== null && imageTag !== void 0 ? imageTag : 'latest'}`
: undefined;
if (fullImageName) {
if (!cacheFrom.includes(fullImageName)) {
// If the cacheFrom options don't include the fullImageName, add it here
// This ensures that when building a PR where the image specified in the action
// isn't included in devcontainer.json (or docker-compose.yml), the action still
// resolves a previous image for the tag as a layer cache (if pushed to a registry)
cacheFrom.splice(0, 0, fullImageName);
const resolvedImageTag = imageTag !== null && imageTag !== void 0 ? imageTag : 'latest';
const imageTagArray = resolvedImageTag.split(',');
const fullImageNameArray = [];
for (const tag of imageTagArray) {
fullImageNameArray.push(`${imageName}:${tag}`);
}
if (imageName) {
if (fullImageNameArray.length === 1) {
if (!cacheFrom.includes(fullImageNameArray[0])) {
// If the cacheFrom options don't include the fullImageName, add it here
// This ensures that when building a PR where the image specified in the action
// isn't included in devcontainer.json (or docker-compose.yml), the action still
// resolves a previous image for the tag as a layer cache (if pushed to a registry)
cacheFrom.splice(0, 0, fullImageNameArray[0]);
}
}
else {
// Don't automatically add --cache-from if multiple image tags are specified
console.log('Not adding --cache-from automatically since multiple image tags were supplied');
}
}
else {
console.log('!! imageTag specified without specifying imageName - ignoring imageTag');
}
const buildArgs = {
workspaceFolder,
imageName: fullImageName,
imageName: fullImageNameArray,
platform,
additionalCacheFroms: cacheFrom,
output: buildxOutput,
Expand Down Expand Up @@ -412,16 +421,21 @@ function runPost() {
return;
}
const imageTag = (_f = task.getInput('imageTag')) !== null && _f !== void 0 ? _f : 'latest';
const imageTagArray = imageTag.split(',');
const platform = task.getInput('platform');
if (platform) {
console.log(`Copying multiplatform image ''${imageName}:${imageTag}...`);
const imageSource = 'oci-archive:/tmp/output.tar';
const imageDest = `docker://${imageName}:${imageTag}`;
yield skopeo_1.copyImage(true, imageSource, imageDest);
for (const tag of imageTagArray) {
console.log(`Copying multiplatform image '${imageName}:${tag}'...`);
const imageSource = `oci-archive:/tmp/output.tar:${tag}`;
const imageDest = `docker://${imageName}:${tag}`;
yield skopeo_1.copyImage(true, imageSource, imageDest);
}
}
else {
console.log(`Pushing image ''${imageName}:${imageTag}...`);
yield docker_1.pushImage(imageName, imageTag);
for (const tag of imageTagArray) {
console.log(`Pushing image '${imageName}:${tag}'...`);
yield docker_1.pushImage(imageName, tag);
}
}
});
}
Expand Down Expand Up @@ -17089,7 +17103,7 @@ function devContainerBuild(args, log) {
args.workspaceFolder,
];
if (args.imageName) {
commandArgs.push('--image-name', args.imageName);
args.imageName.forEach(iName => commandArgs.push('--image-name', iName));
}
if (args.platform) {
commandArgs.push('--platform', args.platform);
Expand Down
2 changes: 1 addition & 1 deletion azdo-task/DevcontainersCi/dist/index.js.map

Large diffs are not rendered by default.

48 changes: 31 additions & 17 deletions azdo-task/DevcontainersCi/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,24 +79,33 @@ function runMain() {
const buildxOutput = platform ? 'type=oci,dest=/tmp/output.tar' : undefined;
const log = (message) => console.log(message);
const workspaceFolder = path_1.default.resolve(checkoutPath, subFolder);
const fullImageName = imageName
? `${imageName}:${imageTag !== null && imageTag !== void 0 ? imageTag : 'latest'}`
: undefined;
if (fullImageName) {
if (!cacheFrom.includes(fullImageName)) {
// If the cacheFrom options don't include the fullImageName, add it here
// This ensures that when building a PR where the image specified in the action
// isn't included in devcontainer.json (or docker-compose.yml), the action still
// resolves a previous image for the tag as a layer cache (if pushed to a registry)
cacheFrom.splice(0, 0, fullImageName);
const resolvedImageTag = imageTag !== null && imageTag !== void 0 ? imageTag : 'latest';
const imageTagArray = resolvedImageTag.split(',');
const fullImageNameArray = [];
for (const tag of imageTagArray) {
fullImageNameArray.push(`${imageName}:${tag}`);
}
if (imageName) {
if (fullImageNameArray.length === 1) {
if (!cacheFrom.includes(fullImageNameArray[0])) {
// If the cacheFrom options don't include the fullImageName, add it here
// This ensures that when building a PR where the image specified in the action
// isn't included in devcontainer.json (or docker-compose.yml), the action still
// resolves a previous image for the tag as a layer cache (if pushed to a registry)
cacheFrom.splice(0, 0, fullImageNameArray[0]);
}
}
else {
// Don't automatically add --cache-from if multiple image tags are specified
console.log('Not adding --cache-from automatically since multiple image tags were supplied');
}
}
else {
console.log('!! imageTag specified without specifying imageName - ignoring imageTag');
}
const buildArgs = {
workspaceFolder,
imageName: fullImageName,
imageName: fullImageNameArray,
platform,
additionalCacheFroms: cacheFrom,
output: buildxOutput,
Expand Down Expand Up @@ -223,16 +232,21 @@ function runPost() {
return;
}
const imageTag = (_f = task.getInput('imageTag')) !== null && _f !== void 0 ? _f : 'latest';
const imageTagArray = imageTag.split(',');
const platform = task.getInput('platform');
if (platform) {
console.log(`Copying multiplatform image ''${imageName}:${imageTag}...`);
const imageSource = 'oci-archive:/tmp/output.tar';
const imageDest = `docker://${imageName}:${imageTag}`;
yield skopeo_1.copyImage(true, imageSource, imageDest);
for (const tag of imageTagArray) {
console.log(`Copying multiplatform image '${imageName}:${tag}'...`);
const imageSource = `oci-archive:/tmp/output.tar:${tag}`;
const imageDest = `docker://${imageName}:${tag}`;
yield skopeo_1.copyImage(true, imageSource, imageDest);
}
}
else {
console.log(`Pushing image ''${imageName}:${imageTag}...`);
yield docker_1.pushImage(imageName, imageTag);
for (const tag of imageTagArray) {
console.log(`Pushing image '${imageName}:${tag}'...`);
yield docker_1.pushImage(imageName, tag);
}
}
});
}
Expand Down
50 changes: 33 additions & 17 deletions azdo-task/DevcontainersCi/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,27 @@ export async function runMain(): Promise<void> {

const log = (message: string): void => console.log(message);
const workspaceFolder = path.resolve(checkoutPath, subFolder);
const fullImageName = imageName
? `${imageName}:${imageTag ?? 'latest'}`
: undefined;
if (fullImageName) {
if (!cacheFrom.includes(fullImageName)) {
// If the cacheFrom options don't include the fullImageName, add it here
// This ensures that when building a PR where the image specified in the action
// isn't included in devcontainer.json (or docker-compose.yml), the action still
// resolves a previous image for the tag as a layer cache (if pushed to a registry)
cacheFrom.splice(0, 0, fullImageName);

const resolvedImageTag = imageTag ?? 'latest';
const imageTagArray = resolvedImageTag.split(',');
const fullImageNameArray: string[] = [];
for (const tag of imageTagArray) {
fullImageNameArray.push(`${imageName}:${tag}`);
}
if (imageName) {
if (fullImageNameArray.length === 1) {
if (!cacheFrom.includes(fullImageNameArray[0])) {
// If the cacheFrom options don't include the fullImageName, add it here
// This ensures that when building a PR where the image specified in the action
// isn't included in devcontainer.json (or docker-compose.yml), the action still
// resolves a previous image for the tag as a layer cache (if pushed to a registry)
cacheFrom.splice(0, 0, fullImageNameArray[0]);
}
} else {
// Don't automatically add --cache-from if multiple image tags are specified
console.log(
'Not adding --cache-from automatically since multiple image tags were supplied',
);
}
} else {
console.log(
Expand All @@ -79,7 +90,7 @@ export async function runMain(): Promise<void> {
}
const buildArgs: DevContainerCliBuildArgs = {
workspaceFolder,
imageName: fullImageName,
imageName: fullImageNameArray,
platform,
additionalCacheFroms: cacheFrom,
output: buildxOutput,
Expand Down Expand Up @@ -234,15 +245,20 @@ export async function runPost(): Promise<void> {
return;
}
const imageTag = task.getInput('imageTag') ?? 'latest';
const imageTagArray = imageTag.split(',');
const platform = task.getInput('platform');
if (platform) {
console.log(`Copying multiplatform image ''${imageName}:${imageTag}...`);
const imageSource = 'oci-archive:/tmp/output.tar';
const imageDest = `docker://${imageName}:${imageTag}`;
for (const tag of imageTagArray) {
console.log(`Copying multiplatform image '${imageName}:${tag}'...`);
const imageSource = `oci-archive:/tmp/output.tar:${tag}`;
const imageDest = `docker://${imageName}:${tag}`;

await copyImage(true, imageSource, imageDest);
await copyImage(true, imageSource, imageDest);
}
} else {
console.log(`Pushing image ''${imageName}:${imageTag}...`);
await pushImage(imageName, imageTag);
for (const tag of imageTagArray) {
console.log(`Pushing image '${imageName}:${tag}'...`);
await pushImage(imageName, tag);
}
}
}
2 changes: 1 addition & 1 deletion azdo-task/DevcontainersCi/task.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
{
"name": "imageTag",
"type": "string",
"label": "Image tag (defaults to latest)",
"label": "One or more comma-separated image tags (defaults to latest)",
"required": false
},
{
Expand Down
2 changes: 1 addition & 1 deletion azdo-task/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ In the example above, the devcontainer-build-run will perform the following step
| Name | Required | Description |
| ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| imageName | true | Image name to use when building the dev container image (including registry) |
| imageTag | false | Image tag to use when building/pushing the dev container image (defaults to `latest`) |
| imageTag | false | One or more comma-separated image tags (defaults to `latest`) |
| subFolder | false | Use this to specify the repo-relative path to the folder containing the dev container (i.e. the folder that contains the `.devcontainer` folder). Defaults to repo root |
| runCmd | true | The command to run after building the dev container image |
| env | false | Specify environment variables to pass to the dev container when run |
Expand Down
6 changes: 4 additions & 2 deletions common/src/dev-container-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export interface DevContainerCliBuildResult
extends DevContainerCliSuccessResult {}
export interface DevContainerCliBuildArgs {
workspaceFolder: string;
imageName?: string;
imageName?: string[];
platform?: string;
additionalCacheFroms?: string[];
userDataFolder?: string;
Expand All @@ -151,7 +151,9 @@ async function devContainerBuild(
args.workspaceFolder,
];
if (args.imageName) {
commandArgs.push('--image-name', args.imageName);
args.imageName.forEach(iName =>
commandArgs.push('--image-name', iName),
);
}
if (args.platform) {
commandArgs.push('--platform', args.platform);
Expand Down
Loading

0 comments on commit c5e74b5

Please sign in to comment.