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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ jobs:
npm install -g @devcontainers/cli
devcontainer build --workspace-folder . --config devcontainer.json --image-name cs50/codespace:${{ github.sha }} --image-name cs50/codespace:latest

- name: Smoke-test (latest)
if: ${{ github.ref == 'refs/heads/main' }}
run: |
bash tests/smoke.sh cs50/codespace:latest

- name: Push (latest) to GitHub Packages
if: ${{ github.ref == 'refs/heads/main' }}
run: |
Expand Down Expand Up @@ -72,6 +77,11 @@ jobs:
npm install -g @devcontainers/cli
devcontainer build --workspace-folder . --config devcontainer.json --image-name cs50/codespace:${{ github.sha }} --image-name cs50/codespace:${{ github.ref_name }}

- name: Smoke-test (${{ github.ref_name }})
if: ${{ github.ref != 'refs/heads/main' }}
run: |
bash tests/smoke.sh cs50/codespace:${{ github.ref_name }}

- name: Squash (${{ github.ref_name }})
if: ${{ github.ref != 'refs/heads/main' }}
run: |
Expand Down
7 changes: 7 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ RUN npm install --global @vscode/vsce yarn && \
mv design50-1.0.0.vsix /opt/cs50/extensions && \
cd /tmp && \
rm --force --recursive design50.vsix && \
git clone https://github.com/cs50/help50.vsix.git && \
cd help50.vsix && \
npm install && \
vsce package && \
mv help50-0.0.1.vsix /opt/cs50/extensions && \
cd /tmp && \
rm --force --recursive help50.vsix && \
npm uninstall --global vsce yarn


Expand Down
7 changes: 6 additions & 1 deletion devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"/opt/cs50/extensions/cs50-0.0.1.vsix",
"/opt/cs50/extensions/design50-1.0.0.vsix",
"/opt/cs50/extensions/ddb50-2.0.0.vsix",
"/opt/cs50/extensions/help50-0.0.1.vsix",
"/opt/cs50/extensions/phpliteadmin-0.0.1.vsix",
"/opt/cs50/extensions/style50-0.0.1.vsix",
"cs50.extension-uninstaller",
Expand Down Expand Up @@ -189,7 +190,11 @@
"terminal.integrated.defaultProfile.linux": "bash",
"terminal.integrated.gpuAcceleration": "off",
"terminal.integrated.profiles.linux": {
"JavaScript Debug Terminal": null
"JavaScript Debug Terminal": null,
"Sysadmins": {
"path": "sudo",
"args": ["bash"]
}
},
"terminal.integrated.persistentSessionReviveProcess": "never",
"terminal.integrated.sendKeybindingsToShell": true,
Expand Down
59 changes: 58 additions & 1 deletion etc/profile.d/codespace.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
# If not root
if [ "$(whoami)" != "root" ]; then
if [ `id -u` -ne 0 ]; then

# Library (from cs50/cli; absent in images built before help50 landed there)
if [ -f /opt/cs50/lib/cli ]; then
. /opt/cs50/lib/cli
fi

# Check if running locally and set $RepositoryName if not already set
if [[ "$CODESPACES" != "true" && -z "$RepositoryName" ]]; then
export RepositoryName=$(ls -1t --color=never /workspaces | tail -1 | sed 's:/*$::')
export LOCAL_WORKSPACE_FOLDER="/workspaces/$RepositoryName"
fi

# Where help50's helpers look for misplaced files (cs50/cli defaults this to $HOME)
export WORKDIR="/workspaces/$RepositoryName"

# Rewrites URLs of the form http://HOST:PORT as https://$CODESPACE_NAME.app.github.dev:PORT
_hostname() {

Expand Down Expand Up @@ -79,4 +87,53 @@ if [ "$(whoami)" != "root" ]; then
http-server() {
command http-server "$@" | _hostname | _version | uniq
}

# help50 hooks, called by _help50 in /etc/profile.d/help50.sh after each command.
# When no local helper has advice, these relay the failed command's output to the
# help50 VS Code extension via command50, which shows a "help50" button in the
# terminal's title bar; clicking it asks the CS50 Duck to explain the error.
# command50 runs detached with output discarded so the prompt isn't delayed and a
# missing extension server degrades silently.
_help50_button() {
( command50 help50.showButton "$1" "$2" > /dev/null 2>&1 & )
_HELP50_BUTTON=1
}
_help50_hide() {
if [[ -n "$_HELP50_BUTTON" ]]; then
( command50 help50.hideButton > /dev/null 2>&1 & )
unset _HELP50_BUTTON
fi
}

# A helper had advice: show it here, as in cs50/cli. No button, since the advice is
# already on screen; a button still showing is about an earlier command.
_helpful() {
for name in n no y yes; do
alias $name=_rhetorical # Intercept answers to the rhetorical question
done
_alert "$(_ansi "$1")"
_help50_hide
}

# No helper matched: offer the duck the failed command's output to explain.
# If there's no output (e.g., grep with no match, or a program exiting 1), there's
# nothing to explain, and any button still showing is about an earlier command.
# $1 is the output, $2 the command line; the duck gets both, as a transcript.
_helpless() {
if [[ -z "${1//[[:space:]]/}" ]]; then
_help50_hide
return
fi
_alert "$(_ansi "🦆 Click \`help50\` above for help with that error.")"
if [[ -n "$2" ]]; then
_help50_button ask "$ $2"$'\n'"$1"
else
_help50_button ask "$1"
fi
}

# Command succeeded: hide the button, if showing
_helped() {
_help50_hide
}
fi
59 changes: 59 additions & 0 deletions tests/smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#!/bin/bash
# Smoke-tests a built cs50/codespace image. Usage: tests/smoke.sh [IMAGE]
# Each check has a timeout so that a regression that hangs the shell fails loudly.

set -o errexit -o errtrace -o nounset -o pipefail
step="startup"
trap 'echo "FAILED: $step" >&2' ERR

IMAGE="${1:-cs50/codespace}"
run() { timeout 60 docker run --rm --env CODESPACES=true --env RepositoryName=smoke "$@"; }
check() { step="$1"; echo "- $step"; }
skip() { echo "- SKIPPED: $1"; }

echo "Checking $IMAGE"

check "login shells start without errors"
test -z "$(run "$IMAGE" bash --login -c 'true' 2>&1)"

check "non-interactive login shell exits (help50 must not start without a terminal)"
run "$IMAGE" bash --login -c 'echo ok' | grep -qx ok
echo true | run --interactive "$IMAGE" bash --login

# The rest applies once the cs50/cli base image carries help50 (cs50/cli#210).
# Branch builds use cs50/cli:amd64 (main), canary uses cs50/cli:canary.
if run "$IMAGE" bash --login -c 'test -f /etc/profile.d/help50.sh -a -f /opt/cs50/lib/cli'; then

check "help50 is inherited from cs50/cli and its hooks are overridden by codespace.sh"
run "$IMAGE" bash --login -c '
test "$(type -P help50)" = /opt/cs50/bin/help50 &&
test "$WORKDIR" = /workspaces/smoke &&
for f in _helped _helpful _helpless _help50_button _alert _ansi; do declare -F "$f" > /dev/null || { echo "missing $f" >&2; exit 1; }; done &&
declare -f _help50_button | grep -q command50'

check "hooks behave: advice is printed, empty output is ignored (no extension server needed)"
run "$IMAGE" bash --login -c '
_helpful "Did you mean \`ls\`?" 2>&1 | grep -q "Did you mean" &&
test -z "$(_helpless "" 2>&1)" &&
test -z "$(_helpless " " 2>&1)" &&
_helpless "cat: x: No such file" 2>&1 | grep -q help50'

# _help50_button discards command50's output, so stub it to see the payload
check "the duck gets the command line and output as a transcript (or output alone without a command)"
run "$IMAGE" bash --login -c '
_alert() { :; }
_help50_button() { printf "%s" "$2"; }
test "$(_helpless "cat: x: No such file" "cat x")" = "$(printf "%s\n%s" "$ cat x" "cat: x: No such file")" &&
test "$(_helpless "cat: x: No such file")" = "cat: x: No such file" &&
test -z "$(_helpless " " "cat x")"'

check "root shells (Sysadmins profile) do not start help50"
run --user root "$IMAGE" bash --login -c 'test -z "${HELP50:-}" && ! declare -F _helpful > /dev/null'
else
skip "help50 checks: base image has no help50 (cs50/cli without #210)"
fi

check "help50 extension is packaged for installation"
run "$IMAGE" bash --login -c 'test -f /opt/cs50/extensions/help50-0.0.1.vsix && command -v command50 > /dev/null'

echo "OK"