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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/go-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ on:
# push/PR only the cases affected by the change run (see parity-runtime).
- cron: "0 6 * * *"

# Least privilege for every job in this workflow: the jobs only check out the repo
# and read the API (setup-task's repo-token). A job that needs more must widen it
# for itself. Without this default, GITHUB_TOKEN falls back to the repository's
# (possibly write) default — see the CodeQL "workflow does not contain permissions"
# rule, which TestWorkflowsDeclarePermissions pins.
permissions:
contents: read

jobs:
lint-and-test:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -109,7 +117,7 @@ jobs:
- uses: actions/upload-artifact@v7
if: always()
with:
name: parity-contract-network-v0.88.0
name: parity-contract-network-v0.89.0
path: artifacts/
if-no-files-found: error
- uses: actions/upload-artifact@v7
Expand Down Expand Up @@ -222,7 +230,7 @@ jobs:
- uses: actions/upload-artifact@v7
if: always() && steps.plan.outputs.run == 'true'
with:
name: parity-runtime-v0.88.0-shard-${{ matrix.shard }}
name: parity-runtime-v0.89.0-shard-${{ matrix.shard }}
path: artifacts/
if-no-files-found: error
# daily only: covdata slice for the cross-lane merge (distinct name per shard).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ GitHub Releases are not supported targets. “Compatible” means compatibility
this scope; it does not mean that every platform or historical upstream behavior is
implemented.

A pinned official TypeScript CLI (`reference/`, currently v0.88.0) is the behavioral
A pinned official TypeScript CLI (`reference/`, currently v0.89.0) is the behavioral
oracle. Roughly 200 cases run commands through both CLIs and compare exit status,
normalized output, and relevant container or registry state. See the
[parity matrix](docs/parity/parity-matrix.yaml) and
Expand Down
12 changes: 11 additions & 1 deletion docs/DIVERGENCES.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Divergences, decisions & accepted limitations

This CLI is validated for behavioral parity with the reference TypeScript
`@devcontainers/cli` (pinned at **v0.88.0**, see [`parity/`](parity/)). Where it
`@devcontainers/cli` (pinned at **v0.89.0**, see [`parity/`](parity/)). Where it
deliberately differs, the difference is recorded here — this is the durable record of
*intentional* departures from the oracle, not a backlog. User-facing additions are
documented in [`go-only-features.md`](go-only-features.md).
Expand Down Expand Up @@ -32,6 +32,16 @@ touches a compared surface, reflected in the parity matrix.
- **`config.build.cacheFrom`** is honored (wired to `--cache-from` after the flag's
values) — matching `singleContainer.ts`. Upstream defines the field; this is a parity
fix, noted here because it was previously a dead field.
- **OCI auth hardening is enforced through `oras-go`, which is stricter by default.**
`--oci-auth-hardening` and `--allow-cross-origin-auth-host` behave as documented
upstream (bearer realms pinned to the registry authority or a trusted auth host,
token endpoints may not redirect), and `ociAuthDiagnostics` reports the same three
flags in `up`/`build`/`read-configuration`. The difference is what happens
**without** the flag: the reference CLI still forwards registry credentials to a
challenge that arrives from another origin, while `oras-go` never does
(GHSA-vh4v-2xq2-g5cg). Hardening off is therefore already safe here; the
diagnostics still report what hardening *would* change, so the flag remains a
faithful compatibility probe.
- **`BUILDKIT_INLINE_CACHE=1`** is omitted when `--cache-to` is an inline exporter
(`/type\s*=\s*inline/i`), matching TS `isBuildxCacheToInline` — a parity fix over the
earlier unconditional build-arg.
Expand Down
20 changes: 17 additions & 3 deletions docs/parity/cli-flags-inventory.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ parser_configuration:
strict: true
demand_command: true

# ─────────────────────────────────────────────────────────────────────────────
# Global flags (yargs `.option(..., { global: true })`: accepted by every command)
# ─────────────────────────────────────────────────────────────────────────────
global_flags:
oci-auth-hardening:
type: boolean
default: false
description: "Restrict OCI bearer authentication realms, registry credential forwarding, and token redirects."
allow-cross-origin-auth-host:
type: string
array: true
description: "Allow an OCI registry to use a cross-origin HTTPS authentication host. Format: <registry-host>=<auth-host>. May be repeated."
notes: "Requires --oci-auth-hardening; each entry must be '<registry-host>=<auth-host>' of bare authorities."

# ─────────────────────────────────────────────────────────────────────────────
# Global validations (shared by multiple commands)
# ─────────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -1140,17 +1154,17 @@ commands:
# =============================================================================
json_output_envelopes:
success_with_container: # up
fields: [outcome, containerId, remoteUser, remoteWorkspaceFolder, composeProjectName, configuration, mergedConfiguration]
fields: [outcome, containerId, remoteUser, remoteWorkspaceFolder, composeProjectName, configuration, mergedConfiguration, ociAuthDiagnostics]
success_setup: # set-up
fields: [outcome, configuration, mergedConfiguration]
success_build: # build
fields: [outcome, imageName]
fields: [outcome, imageName, ociAuthDiagnostics]
success_run_user_commands: # run-user-commands
fields: [outcome, result]
error: # all commands with outcome envelope
fields: [outcome, message, description, containerId, disallowedFeatureId, didStopContainer, learnMoreUrl]
read_configuration: # read-configuration (NO outcome envelope)
fields: [configuration, workspace, featuresConfiguration, mergedConfiguration]
fields: [configuration, workspace, featuresConfiguration, mergedConfiguration, ociAuthDiagnostics]

# =============================================================================
# ENVIRONMENT VARIABLES READ
Expand Down
70 changes: 63 additions & 7 deletions docs/parity/parity-matrix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ meta:
scope:
languages: [ts, go]
commands: [features-info, read-configuration, exec, run-user-commands, set-up, up, build]
global_flags: [oci-auth-hardening, allow-cross-origin-auth-host]
lanes:
contract:
description: "Parsing, required args, enums, error formats, output shape"
Expand Down Expand Up @@ -491,6 +492,61 @@ initial_cases:
class: format-validation
current_status: match

- id: read-configuration.oci-auth-hardening-accepted
lane: contract
command: read-configuration
priority: p0
docker_required: false
ts_cmd: "--oci-auth-hardening read-configuration --workspace-folder src/test/configs/image"
asserts: [exit_code, stdout_normalized]
class: global-flags
current_status: match
notes: "0.89 global flag: accepted before the command name and the output carries the ociAuthDiagnostics envelope field (all false when no registry is contacted)."

- id: read-configuration.cross-origin-auth-host-requires-hardening
lane: contract
command: read-configuration
priority: p0
docker_required: false
ts_cmd: "--allow-cross-origin-auth-host registry.example=auth.example read-configuration --workspace-folder src/test/configs/image"
asserts: [exit_code, stderr_normalized]
class: global-flags
current_status: match
notes: "0.89 yargs .check(): --allow-cross-origin-auth-host requires --oci-auth-hardening."

- id: read-configuration.cross-origin-auth-host-invalid-pair
lane: contract
command: read-configuration
priority: p1
docker_required: false
ts_cmd: "--oci-auth-hardening --allow-cross-origin-auth-host bad read-configuration --workspace-folder src/test/configs/image"
asserts: [exit_code, stderr_normalized]
class: global-flags
current_status: match
notes: "0.89: each entry must be '<registry-host>=<auth-host>'."

- id: read-configuration.cross-origin-auth-host-invalid-authority
lane: contract
command: read-configuration
priority: p1
docker_required: false
ts_cmd: "--oci-auth-hardening --allow-cross-origin-auth-host a/b=c read-configuration --workspace-folder src/test/configs/image"
asserts: [exit_code, stderr_normalized]
class: global-flags
current_status: match
notes: "0.89: both sides of the mapping must be bare authorities."

- id: read-configuration.global-options-consume-one-argument
lane: contract
command: read-configuration
priority: p1
docker_required: false
ts_cmd: "--oci-auth-hardening --allow-cross-origin-auth-host registry.example=auth.example read-configuration --workspace-folder src/test/configs/image"
asserts: [exit_code, stdout_normalized]
class: global-flags
current_status: match
notes: "Mirrors the oracle's own 'Global options consume exactly one argument' test (src/test/cli.test.ts), asserted on read-configuration because yargs and cobra render --help differently: the repeatable flag must consume exactly one value and leave the subcommand intact."

- id: read-configuration.terminal-columns-implies-rows
lane: contract
command: read-configuration
Expand Down Expand Up @@ -1288,7 +1344,7 @@ initial_cases:
docker_required: true
network_required: true
ts_cmd: "up --workspace-folder src/test/configs/app-port --buildkit=never --skip-post-create --id-label parity.case=${PARITY_CASE_ID}-${PARITY_SIDE} --remove-existing-container"
verify_cmd: "P=$(docker inspect --format '{{json .HostConfig.PortBindings}}' ${CONTAINER_ID}); echo \"$P\" | grep -q '\"HostPort\":\"3000\"' && echo \"$P\" | grep -q '\"HostPort\":\"8080\"' && printf ok"
verify_cmd: "P=$(docker inspect --format '{{json .HostConfig.PortBindings}}' ${CONTAINER_ID}); echo \"$P\" | grep -q '\"HostPort\":\"31300\"' && echo \"$P\" | grep -q '\"HostPort\":\"31808\"' && printf ok"
cleanup_cmd: "IDS=$(docker ps -aq --filter label=parity.case=${PARITY_CASE_ID}-${PARITY_SIDE}); [ -n \"$IDS\" ] && docker rm -f $IDS >/dev/null 2>&1 || true"
asserts: [exit_code, stdout_normalized]
class: up-runtime
Expand Down Expand Up @@ -1484,13 +1540,13 @@ initial_cases:
docker_required: true
network_required: true
ts_cmd: "up --workspace-folder src/test/configs/compose-image-with-features --buildkit=never --omit-config-remote-env-from-metadata --id-label parity.case=${PARITY_CASE_ID}-${PARITY_SIDE} --remove-existing-container"
setup_cmd: "docker pull --platform linux/amd64 mongo:latest >/dev/null; echo DOCKER_DEFAULT_PLATFORM=linux/amd64"
setup_cmd: "docker pull --platform linux/amd64 redis:alpine >/dev/null; echo DOCKER_DEFAULT_PLATFORM=linux/amd64"
verify_cmd: "test \"$(docker exec ${CONTAINER_ID} cat /postCreateCommand.txt)\" = \"Val: ENV\" && ! docker inspect --format '{{ index .Config.Labels \"devcontainer.metadata\" }}' ${CONTAINER_ID} | grep -q 'TEST_ESCAPING' && printf ok"
cleanup_cmd: "docker compose --project-name ${COMPOSE_PROJECT_NAME} -f src/test/configs/compose-image-with-features/.devcontainer/docker-compose.yml down >/dev/null 2>&1 || true"
asserts: [exit_code, stdout_normalized]
class: up-runtime
current_status: match
notes: "Unblocked by pinning the harness setup with `docker pull --platform linux/amd64 mongo:latest` before compose."
notes: "Unblocked by pinning the harness setup with `docker pull --platform linux/amd64 redis:alpine` (the fixture's db service) before compose."

- id: up.compose-dockerfile-with-features-omit-config-remote-env-from-metadata-success
lane: runtime
Expand All @@ -1499,13 +1555,13 @@ initial_cases:
docker_required: true
network_required: true
ts_cmd: "up --workspace-folder src/test/configs/compose-Dockerfile-with-features --buildkit=never --omit-config-remote-env-from-metadata --id-label parity.case=${PARITY_CASE_ID}-${PARITY_SIDE} --remove-existing-container"
setup_cmd: "docker pull --platform linux/amd64 mongo:latest >/dev/null; echo DOCKER_DEFAULT_PLATFORM=linux/amd64"
setup_cmd: "docker pull --platform linux/amd64 redis:alpine >/dev/null; echo DOCKER_DEFAULT_PLATFORM=linux/amd64"
verify_cmd: "test \"$(docker exec ${CONTAINER_ID} cat /postCreateCommand.txt)\" = \"Val: ENV\" && ! docker inspect --format '{{ index .Config.Labels \"devcontainer.metadata\" }}' ${CONTAINER_ID} | grep -q 'TEST_ESCAPING' && printf ok"
cleanup_cmd: "docker compose --project-name ${COMPOSE_PROJECT_NAME} -f src/test/configs/compose-Dockerfile-with-features/.devcontainer/docker-compose.yml down >/dev/null 2>&1 || true"
asserts: [exit_code, stdout_normalized]
class: up-runtime
current_status: match
notes: "Closed out after aligning the metadata of Dockerfile+features builds; the setup keeps `docker pull --platform linux/amd64 mongo:latest` to avoid platform drift in compose."
notes: "Closed out after aligning the metadata of Dockerfile+features builds; the setup keeps `docker pull --platform linux/amd64 redis:alpine` (the fixture's db service) to avoid platform drift in compose."

- id: up.compose-dockerfile-without-features-omit-config-remote-env-from-metadata-success
lane: runtime
Expand Down Expand Up @@ -1673,7 +1729,7 @@ initial_cases:
docker_required: true
network_required: true
ts_cmd: "up --workspace-folder src/test/configs/compose-Dockerfile-with-features --buildkit=never --user-data-folder ${USER_DATA_FOLDER}"
setup_cmd: "docker pull --platform linux/amd64 mongo:latest >/dev/null; USER_DATA_FOLDER=/tmp/parity-user-data-${PARITY_CASE_ID}-${PARITY_SIDE}; rm -rf ${USER_DATA_FOLDER}; mkdir -p ${USER_DATA_FOLDER}; if [ \"${PARITY_SIDE}\" = \"ts\" ]; then ${PARITY_CLI_TS} up --workspace-folder src/test/configs/compose-Dockerfile-with-features --buildkit=never --user-data-folder ${USER_DATA_FOLDER} --remove-existing-container >/dev/null; else ${PARITY_CLI_GO} up --workspace-folder src/test/configs/compose-Dockerfile-with-features --buildkit=never --user-data-folder ${USER_DATA_FOLDER} --remove-existing-container >/dev/null; fi; ORIGINAL_CONTAINER_ID=$(docker ps -aq --filter label=com.docker.compose.project=compose-dockerfile-with-features_devcontainer --filter label=com.docker.compose.service=app | head -n1); docker compose --project-name compose-dockerfile-with-features_devcontainer stop >/dev/null; echo DOCKER_DEFAULT_PLATFORM=linux/amd64; echo USER_DATA_FOLDER=${USER_DATA_FOLDER}; echo ORIGINAL_CONTAINER_ID=${ORIGINAL_CONTAINER_ID}"
setup_cmd: "docker pull --platform linux/amd64 redis:alpine >/dev/null; USER_DATA_FOLDER=/tmp/parity-user-data-${PARITY_CASE_ID}-${PARITY_SIDE}; rm -rf ${USER_DATA_FOLDER}; mkdir -p ${USER_DATA_FOLDER}; if [ \"${PARITY_SIDE}\" = \"ts\" ]; then ${PARITY_CLI_TS} up --workspace-folder src/test/configs/compose-Dockerfile-with-features --buildkit=never --user-data-folder ${USER_DATA_FOLDER} --remove-existing-container >/dev/null; else ${PARITY_CLI_GO} up --workspace-folder src/test/configs/compose-Dockerfile-with-features --buildkit=never --user-data-folder ${USER_DATA_FOLDER} --remove-existing-container >/dev/null; fi; ORIGINAL_CONTAINER_ID=$(docker ps -aq --filter label=com.docker.compose.project=compose-dockerfile-with-features_devcontainer --filter label=com.docker.compose.service=app | head -n1); docker compose --project-name compose-dockerfile-with-features_devcontainer stop >/dev/null; echo DOCKER_DEFAULT_PLATFORM=linux/amd64; echo USER_DATA_FOLDER=${USER_DATA_FOLDER}; echo ORIGINAL_CONTAINER_ID=${ORIGINAL_CONTAINER_ID}"
verify_cmd: "test \"${CONTAINER_ID#$ORIGINAL_CONTAINER_ID}\" != \"${CONTAINER_ID}\" && test \"$(find ${USER_DATA_FOLDER}/docker-compose -maxdepth 1 -type f | wc -l | tr -d ' ')\" = \"2\" && find ${USER_DATA_FOLDER}/docker-compose -maxdepth 1 -type f -name 'docker-compose.devcontainer.build-*' | grep -q . && find ${USER_DATA_FOLDER}/docker-compose -maxdepth 1 -type f -name 'docker-compose.devcontainer.containerFeatures-*' | grep -q . && printf ok"
cleanup_cmd: "docker compose --project-name compose-dockerfile-with-features_devcontainer -f src/test/configs/compose-Dockerfile-with-features/.devcontainer/docker-compose.yml down >/dev/null 2>&1 || true; rm -rf ${USER_DATA_FOLDER}"
asserts: [exit_code, stdout_normalized]
Expand All @@ -1690,7 +1746,7 @@ initial_cases:
docker_required: true
network_required: true
ts_cmd: "up --workspace-folder src/test/configs/compose-Dockerfile-with-features --buildkit=never --user-data-folder ${USER_DATA_FOLDER}"
setup_cmd: "docker pull --platform linux/amd64 mongo:latest >/dev/null; USER_DATA_FOLDER=/tmp/parity-user-data-reset-${PARITY_CASE_ID}-${PARITY_SIDE}; rm -rf ${USER_DATA_FOLDER}; mkdir -p ${USER_DATA_FOLDER}; if [ \"${PARITY_SIDE}\" = \"ts\" ]; then ${PARITY_CLI_TS} up --workspace-folder src/test/configs/compose-Dockerfile-with-features --buildkit=never --user-data-folder ${USER_DATA_FOLDER} --remove-existing-container >/dev/null; else ${PARITY_CLI_GO} up --workspace-folder src/test/configs/compose-Dockerfile-with-features --buildkit=never --user-data-folder ${USER_DATA_FOLDER} --remove-existing-container >/dev/null; fi; ORIGINAL_CONTAINER_ID=$(docker ps -aq --filter label=com.docker.compose.project=compose-dockerfile-with-features_devcontainer --filter label=com.docker.compose.service=app | head -n1); docker compose --project-name compose-dockerfile-with-features_devcontainer stop >/dev/null; rm -rf ${USER_DATA_FOLDER}; mkdir -p ${USER_DATA_FOLDER}; echo DOCKER_DEFAULT_PLATFORM=linux/amd64; echo USER_DATA_FOLDER=${USER_DATA_FOLDER}; echo ORIGINAL_CONTAINER_ID=${ORIGINAL_CONTAINER_ID}"
setup_cmd: "docker pull --platform linux/amd64 redis:alpine >/dev/null; USER_DATA_FOLDER=/tmp/parity-user-data-reset-${PARITY_CASE_ID}-${PARITY_SIDE}; rm -rf ${USER_DATA_FOLDER}; mkdir -p ${USER_DATA_FOLDER}; if [ \"${PARITY_SIDE}\" = \"ts\" ]; then ${PARITY_CLI_TS} up --workspace-folder src/test/configs/compose-Dockerfile-with-features --buildkit=never --user-data-folder ${USER_DATA_FOLDER} --remove-existing-container >/dev/null; else ${PARITY_CLI_GO} up --workspace-folder src/test/configs/compose-Dockerfile-with-features --buildkit=never --user-data-folder ${USER_DATA_FOLDER} --remove-existing-container >/dev/null; fi; ORIGINAL_CONTAINER_ID=$(docker ps -aq --filter label=com.docker.compose.project=compose-dockerfile-with-features_devcontainer --filter label=com.docker.compose.service=app | head -n1); docker compose --project-name compose-dockerfile-with-features_devcontainer stop >/dev/null; rm -rf ${USER_DATA_FOLDER}; mkdir -p ${USER_DATA_FOLDER}; echo DOCKER_DEFAULT_PLATFORM=linux/amd64; echo USER_DATA_FOLDER=${USER_DATA_FOLDER}; echo ORIGINAL_CONTAINER_ID=${ORIGINAL_CONTAINER_ID}"
verify_cmd: "docker inspect ${CONTAINER_ID} >/dev/null && printf ok"
cleanup_cmd: "docker compose --project-name compose-dockerfile-with-features_devcontainer -f src/test/configs/compose-Dockerfile-with-features/.devcontainer/docker-compose.yml down >/dev/null 2>&1 || true; rm -rf ${USER_DATA_FOLDER}"
asserts: [exit_code, stdout_normalized]
Expand Down
7 changes: 5 additions & 2 deletions internal/cli/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ func runBuild(ctx context.Context, out Output, opts *buildOpts) error {
Format: opts.logFormat,
Writer: os.Stderr,
})
// Route the OCI auth diagnostic lines at this command's logger.
ociAuthPolicy(ctx).SetLogger(logger)

// Load config
loadResult, err := config.LoadDevContainerConfig(workspaceFolder, configPath, "")
Expand Down Expand Up @@ -243,8 +245,9 @@ func runBuild(ctx context.Context, out Output, opts *buildOpts) error {
}

return writeSuccessJSON(out, map[string]interface{}{
"outcome": "success",
"imageName": imageNameResult,
"outcome": "success",
"imageName": imageNameResult,
"ociAuthDiagnostics": ociAuthDiagnostics(ctx),
})
}

Expand Down
Loading
Loading