diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0d893c1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,48 @@ +name: Release + +on: + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., 1.0.0)' + required: true + type: string + +jobs: + release: + runs-on: ubuntu-latest + permissions: + contents: write + env: + VERSION: ${{ inputs.version }} + steps: + - name: Validate version format + run: | + if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then + echo "Error: Invalid version format. Use semver (e.g., 1.0.0 or 1.0.0-beta.1)" + exit 1 + fi + + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '18' + + - name: Update version + working-directory: packages/canton-devenv-start + run: jq --arg v "$VERSION" '.version = $v' package.json > tmp.json && mv tmp.json package.json + + - name: Create tarball + working-directory: packages/canton-devenv-start + run: | + npm pack + mv canton-devenv-start-*.tgz devenv-$VERSION.tgz + + - name: Create release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ inputs.version }} + name: v${{ inputs.version }} + files: packages/canton-devenv-start/devenv-${{ inputs.version }}.tgz + generate_release_notes: true diff --git a/.github/workflows/test-devcontainers.yml b/.github/workflows/test-devcontainers.yml new file mode 100644 index 0000000..05fa1ef --- /dev/null +++ b/.github/workflows/test-devcontainers.yml @@ -0,0 +1,25 @@ +name: Test DevContainers + +on: [pull_request] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + variant: [stable, latest] + steps: + - uses: actions/checkout@v4 + - name: Extract SDK version + id: sdk + run: | + VERSION=$(jq -r '.build.args.DAML_SDK_VERSION' \ + packages/canton-devenv-start/templates/.devcontainer/${{ matrix.variant }}/devcontainer.json) + echo "version=$VERSION" >> $GITHUB_OUTPUT + - name: Build ${{ matrix.variant }} + run: | + docker build \ + --build-arg DAML_SDK_VERSION=${{ steps.sdk.outputs.version }} \ + -f packages/canton-devenv-start/templates/.devcontainer/Dockerfile \ + packages/canton-devenv-start/templates/.devcontainer diff --git a/packages/canton-devenv-start/README.md b/packages/canton-devenv-start/README.md index 40ff165..5cacbcc 100644 --- a/packages/canton-devenv-start/README.md +++ b/packages/canton-devenv-start/README.md @@ -13,11 +13,16 @@ bunx devenv-init [options] - `--force` overwrite existing files ## Output -- `.devcontainer/` with Dockerfile, devcontainer.json, `install-daml-vsix.sh` +- `.devcontainer/` with: + - `Dockerfile` (shared) + - `latest/devcontainer.json` (SDK 3.x) + - `stable/devcontainer.json` (SDK 2.x) + - `install-daml-vsix.sh` - `.gitignore` tuned for DAML artifacts -- Empty workspace ready for `dpm new .` -Open the folder in VS Code/Cursor, “Reopen in Container,” and run `dpm build` once to warm the language server. +## Getting Started +1. Open folder in VS Code/Cursor +2. "Reopen in Container" → choose **latest** or **stable** ## Local development 1. From repo root: `bunx --bun link` (aka `bun link`). diff --git a/packages/canton-devenv-start/bin/index.js b/packages/canton-devenv-start/bin/index.js index f1577c3..ac42703 100755 --- a/packages/canton-devenv-start/bin/index.js +++ b/packages/canton-devenv-start/bin/index.js @@ -27,8 +27,19 @@ function parseArgs(argv) { // Security: Prevent path traversal attacks // Allow absolute paths but validate they're not system directories - const forbiddenPaths = ['/etc', '/usr', '/bin', '/sbin', '/var', '/sys', '/proc']; - const isSystemPath = forbiddenPaths.some(fp => normalizedPath === fp || normalizedPath.startsWith(fp + '/')); + const forbiddenPaths = [ + '/etc', '/usr', '/bin', '/sbin', '/var', '/sys', '/proc', + '/root', '/boot', '/lib', '/lib64', '/opt', + 'C:\\Windows', 'C:\\Program Files', 'C:\\Program Files (x86)', + 'C:\\ProgramData', 'C:\\System32' + ]; + const isSystemPath = forbiddenPaths.some(fp => { + const normalizedFp = path.normalize(fp); + return normalizedPath === normalizedFp || + normalizedPath.startsWith(normalizedFp + path.sep) || + normalizedPath.toLowerCase() === normalizedFp.toLowerCase() || + normalizedPath.toLowerCase().startsWith(normalizedFp.toLowerCase() + path.sep); + }); if (isSystemPath) { console.error(`Error: Cannot write to system directory: ${normalizedPath}`); @@ -108,16 +119,22 @@ function copyEntry(src, dest, opts, results) { try { fs.copyFileSync(src, dest); - // Only copy file permissions if not on Windows + // Only set file permissions if not on Windows // Windows doesn't support Unix-style permissions if (process.platform !== 'win32') { - // Sanitize permissions: remove execute bit unless source is explicitly executable - const sanitizedMode = stats.mode & 0o666; // Only keep read/write permissions - if (stats.mode & 0o111) { // If any execute bit is set in source - // Only preserve execute for owner, not group/others - fs.chmodSync(dest, sanitizedMode | 0o100); - } else { + // Check if this is a shell script by extension + const isShellScript = /\.(sh|bash)$/i.test(src); + + if (isShellScript) { + // Shell scripts should always be executable (owner execute + read/write) + fs.chmodSync(dest, 0o755); + } else if (stats.mode & 0o111) { + // If source has any execute bit, preserve owner execute only + const sanitizedMode = (stats.mode & 0o666) | 0o100; fs.chmodSync(dest, sanitizedMode); + } else { + // Regular files: read/write only + fs.chmodSync(dest, stats.mode & 0o666); } } results.copied.push(dest); diff --git a/packages/canton-devenv-start/templates/.devcontainer/Dockerfile b/packages/canton-devenv-start/templates/.devcontainer/Dockerfile index 892101f..6120aa3 100644 --- a/packages/canton-devenv-start/templates/.devcontainer/Dockerfile +++ b/packages/canton-devenv-start/templates/.devcontainer/Dockerfile @@ -1,5 +1,16 @@ FROM eclipse-temurin:17-jdk +# Build argument for SDK version - MUST be set by devcontainer.json +ARG DAML_SDK_VERSION + +# Validate that DAML_SDK_VERSION is provided +RUN if [ -z "$DAML_SDK_VERSION" ]; then \ + echo "ERROR: DAML_SDK_VERSION build argument is required" && exit 1; \ + fi + +# Persist DAML_SDK_VERSION as ENV for runtime use +ENV DAML_SDK_VERSION=${DAML_SDK_VERSION} + ENV DEBIAN_FRONTEND=noninteractive \ TERM=dumb \ NODE_VERSION=18 @@ -30,9 +41,12 @@ RUN useradd -m -u 1001 -s /bin/bash daml || true && \ USER daml WORKDIR /home/daml -# Install DAML SDK via dpm -ENV DPM_HOME=/home/daml/.dpm -ENV PATH="${DPM_HOME}/bin:/home/daml/.daml/bin:${PATH}" -RUN curl -sSL https://get.digitalasset.com/install/install.sh | sh +# Install DAML SDK from GitHub releases +RUN echo "Installing DAML SDK version ${DAML_SDK_VERSION} from GitHub..." && \ + curl -sSL "https://github.com/digital-asset/daml/releases/download/v${DAML_SDK_VERSION}/daml-sdk-${DAML_SDK_VERSION}-linux.tar.gz" -o daml-sdk.tar.gz && \ + mkdir -p /home/daml/.daml/sdk/${DAML_SDK_VERSION} && \ + tar -xzf daml-sdk.tar.gz -C /home/daml/.daml/sdk/${DAML_SDK_VERSION} --strip-components=1 && \ + rm daml-sdk.tar.gz && \ + /home/daml/.daml/sdk/${DAML_SDK_VERSION}/daml/daml install /home/daml/.daml/sdk/${DAML_SDK_VERSION} --install-with-custom-version=${DAML_SDK_VERSION} WORKDIR /workspaces diff --git a/packages/canton-devenv-start/templates/.devcontainer/daml-lsp-restart.sh b/packages/canton-devenv-start/templates/.devcontainer/daml-lsp-restart.sh new file mode 100755 index 0000000..2ac968d --- /dev/null +++ b/packages/canton-devenv-start/templates/.devcontainer/daml-lsp-restart.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +echo "Stopping any running DAML LSP processes..." +pkill -f "damlc (multi-ide|ide)" >/dev/null 2>&1 || true + +echo "Starting DAML LSP in multi-ide mode..." +/home/daml/.dpm/bin/dpm damlc multi-ide --optOutTelemetry --log-level=Debug >/tmp/daml-lsp.log 2>&1 & + +echo "DAML LSP restarted. Logs redirected to /tmp/daml-lsp.log" diff --git a/packages/canton-devenv-start/templates/.devcontainer/install-daml-vsix.sh b/packages/canton-devenv-start/templates/.devcontainer/install-daml-vsix.sh index 5005255..f575ea6 100755 --- a/packages/canton-devenv-start/templates/.devcontainer/install-daml-vsix.sh +++ b/packages/canton-devenv-start/templates/.devcontainer/install-daml-vsix.sh @@ -1,7 +1,11 @@ #!/usr/bin/env bash set -euo pipefail -DAML_SDK_VERSION="${DAML_SDK_VERSION:-3.4.7}" +# DAML_SDK_VERSION is set by the Dockerfile as an ENV variable +if [[ -z "${DAML_SDK_VERSION:-}" ]]; then + echo "[daml-vsix] Error: DAML_SDK_VERSION environment variable not set" + exit 1 +fi find_vsix() { local cache_path @@ -30,8 +34,10 @@ find_code_server() { ) for base in "${roots[@]}"; do [[ -d "$base" ]] || continue - local first - first=$(ls -d "$base"/* 2>/dev/null | head -n1 || true) + local first="" + for entry in "$base"/*; do + [[ -e "$entry" ]] && first="$entry" && break + done [[ -n "$first" ]] || continue if [[ -x "$first/bin/code-server" ]]; then CODE_SERVER_BIN="$first/bin/code-server" diff --git a/packages/canton-devenv-start/templates/.devcontainer/latest/devcontainer.json b/packages/canton-devenv-start/templates/.devcontainer/latest/devcontainer.json new file mode 100644 index 0000000..51e2e22 --- /dev/null +++ b/packages/canton-devenv-start/templates/.devcontainer/latest/devcontainer.json @@ -0,0 +1,48 @@ +{ + "name": "Canton DAML Dev (latest)", + "build": { + "dockerfile": "../Dockerfile", + "context": "../..", + "args": { + "DAML_SDK_VERSION": "3.4.8" + } + }, + "remoteUser": "daml", + "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", + "runArgs": [ + "--init" + ], + "mounts": [ + "source=vscode-server-extensions,target=/home/daml/.vscode-server/extensions,type=volume" + ], + "containerEnv": { + "DPM_HOME": "/home/daml/.dpm" + }, + "remoteEnv": { + "PATH": "${containerEnv:PATH}:/home/daml/.dpm/bin:/home/daml/.daml/bin" + }, + "postAttachCommand": "/workspaces/${localWorkspaceFolderBasename}/.devcontainer/install-daml-vsix.sh", + "customizations": { + "vscode": { + "extensions": [ + "ms-vscode-remote.remote-containers", + "mkhl.direnv" + ], + "settings": { + "editor.formatOnSave": true, + "terminal.integrated.defaultProfile.osx": "bash", + "editor.codeLens": true, + "daml-language-server.trace.server": "verbose" + } + } + }, + "forwardPorts": [ + 5011, + 5012, + 5021, + 5022, + 5018, + 5019, + 7500 + ] +} diff --git a/packages/canton-devenv-start/templates/.devcontainer/devcontainer.json b/packages/canton-devenv-start/templates/.devcontainer/stable/devcontainer.json similarity index 77% rename from packages/canton-devenv-start/templates/.devcontainer/devcontainer.json rename to packages/canton-devenv-start/templates/.devcontainer/stable/devcontainer.json index 98406d3..fb3544c 100644 --- a/packages/canton-devenv-start/templates/.devcontainer/devcontainer.json +++ b/packages/canton-devenv-start/templates/.devcontainer/stable/devcontainer.json @@ -1,8 +1,11 @@ { - "name": "Canton DAML Dev", + "name": "Canton DAML Dev (stable)", "build": { - "dockerfile": "Dockerfile", - "context": ".." + "dockerfile": "../Dockerfile", + "context": "../..", + "args": { + "DAML_SDK_VERSION": "2.10.2" + } }, "remoteUser": "daml", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", @@ -12,13 +15,9 @@ "mounts": [ "source=vscode-server-extensions,target=/home/daml/.vscode-server/extensions,type=volume" ], - "containerEnv": { - "DAML_SDK_VERSION": "3.4.7" - }, "remoteEnv": { "PATH": "${containerEnv:PATH}:/home/daml/.daml/bin" }, - "postCreateCommand": "dpm --version && echo 'DAML SDK installed successfully' || echo 'Warning: DAML not found'", "postAttachCommand": "/workspaces/${localWorkspaceFolderBasename}/.devcontainer/install-daml-vsix.sh", "customizations": { "vscode": {