Skip to content
Draft
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
236 changes: 235 additions & 1 deletion .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,22 @@ name: Tests
on:
pull_request:
workflow_dispatch:
inputs:
core_tag:
description: >-
Core release tag to qualify with this provider checkout (for example
v1.25.0-beta.3). Runs the release-qualification job.
required: false
type: string
provider_tag:
description: Published SSH provider release to qualify
required: true
default: v1.1.47
type: string

jobs:
test:
name: Run Integration Tests
name: Integration tests (compatibility core v1.24.0, not release qualification)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
Expand Down Expand Up @@ -68,3 +80,225 @@ jobs:
go-version-file: go.mod
- name: test redirected pipe bytes and cancellation
run: go test ./pkg/ssh -run TestWindows -timeout 60s

release-qualification:
name: Release qualification (${{ matrix.core_artifact }}, explicit pair)
if: github.event_name == 'workflow_dispatch' && inputs.core_tag != ''
runs-on: ubuntu-latest
timeout-minutes: 240
strategy:
fail-fast: false
matrix:
core_artifact: [cli, desktop]
permissions:
contents: read
env:
CORE_TAG: ${{ inputs.core_tag }}
CORE_REPO: devsy-org/devsy
PROVIDER_TAG: ${{ inputs.provider_tag }}
CORE_ARTIFACT: ${{ matrix.core_artifact }}
# Merge commits that must be ancestors of the tested pair (issue #1621).
CORE_FIX_SHA: e22be00149ace87d512de0ec953541e3f7642c69
PROVIDER_FIX_SHA: d31fed1b2695861b76e922c530ad8a2436f5acb9
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false

- name: choose the qualification artifact directory
run: echo "QUALIFICATION_DIR=$RUNNER_TEMP/qualification" >> "$GITHUB_ENV"

- name: setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
with:
go-version-file: go.mod

- name: resolve and verify the candidate core
run: |
set -euo pipefail
if ! [[ "$CORE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]]; then
echo "::error::core_tag must look like v1.2.3 or v1.2.3-beta.1"
exit 1
fi
mkdir -p "$QUALIFICATION_DIR/core"

core_sha="$(gh api "repos/$CORE_REPO/commits/$CORE_TAG" --jq .sha)"
core_status="$(gh api "repos/$CORE_REPO/compare/$CORE_FIX_SHA...$core_sha" --jq .status)"
case "$core_status" in
ahead | identical) ;;
*)
echo "::error::core $CORE_TAG ($core_sha) does not contain $CORE_FIX_SHA (compare status: $core_status)"
exit 1
;;
esac
provider_sha="$(git rev-parse --verify "refs/tags/$PROVIDER_TAG^{commit}")"
git merge-base --is-ancestor "$PROVIDER_FIX_SHA" "$provider_sha" || {
echo "::error::provider $PROVIDER_TAG does not contain $PROVIDER_FIX_SHA"
exit 1
}

case "$CORE_ARTIFACT" in
cli) asset=devsy-linux-amd64 ;;
desktop) asset=Devsy_linux_amd64.deb ;;
*) echo "::error::unknown core artifact $CORE_ARTIFACT"; exit 1 ;;
esac
gh release download "$CORE_TAG" --repo "$CORE_REPO" \
--pattern "$asset" --dir "$QUALIFICATION_DIR/core" --clobber
artifact="$QUALIFICATION_DIR/core/$asset"
artifact_sha256="$(sha256sum "$artifact" | cut -d' ' -f1)"
published_digest="$(gh api "repos/$CORE_REPO/releases/tags/$CORE_TAG" \
--jq ".assets[] | select(.name == \"$asset\") | .digest")"
if [ "$published_digest" != "sha256:$artifact_sha256" ]; then
echo "::error::downloaded $asset sha256:$artifact_sha256 differs from the published digest '$published_digest'"
exit 1
fi
core_binary="$artifact"
if [ "$CORE_ARTIFACT" = desktop ]; then
dpkg-deb --extract "$artifact" "$QUALIFICATION_DIR/core/desktop"
core_binary="$QUALIFICATION_DIR/core/desktop/opt/Devsy/resources/bin/devsy"
fi
test -f "$core_binary"
chmod 0755 "$core_binary"
core_sha256="$(sha256sum "$core_binary" | cut -d' ' -f1)"
core_version="$("$core_binary" --version)"
if [ "$core_version" != "$CORE_TAG" ]; then
echo "::error::core binary reports '$core_version', expected '$CORE_TAG'"
exit 1
fi

{
echo "DEVSY_E2E_BINARY=$core_binary"
echo "DEVSY_E2E_CORE_TAG=$CORE_TAG"
echo "DEVSY_E2E_CORE_SHA=$core_sha"
echo "DEVSY_E2E_CORE_VERSION=$core_version"
echo "DEVSY_E2E_PROVIDER_TAG=$PROVIDER_TAG"
echo "DEVSY_E2E_PROVIDER_SHA=$provider_sha"
echo "CORE_SHA256=$core_sha256"
} >> "$GITHUB_ENV"
{
echo "core tag: $CORE_TAG"
echo "core sha: $core_sha"
echo "core artifact: $asset"
echo "core artifact source: https://github.com/$CORE_REPO/releases/download/$CORE_TAG/$asset"
echo "tested binary platform: linux/amd64"
echo "core artifact sha256: $artifact_sha256 (matches published release digest)"
echo "tested core binary sha256: $core_sha256"
echo "core version: $core_version"
echo "core fix $CORE_FIX_SHA is an ancestor (compare status: $core_status)"
echo "provider tag: $PROVIDER_TAG"
echo "provider sha: $provider_sha"
echo "qualification harness sha: $(git rev-parse HEAD)"
echo "provider fix $PROVIDER_FIX_SHA is an ancestor"
} | tee "$QUALIFICATION_DIR/pair-verification.txt" >> "$GITHUB_STEP_SUMMARY"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: install login shells
run: sudo apt-get update && sudo apt-get install -y bash zsh fish openssh-server

- name: download and verify the released provider
run: |
set -euo pipefail
mkdir -p dist/build_linux_amd64_v1
asset=devsy-provider-ssh-linux-amd64
gh release download "$PROVIDER_TAG" --repo "$GITHUB_REPOSITORY" \
--pattern "$asset" --dir dist/build_linux_amd64_v1 --clobber
gh release download "$PROVIDER_TAG" --repo "$GITHUB_REPOSITORY" \
--pattern checksums.txt --dir dist --clobber
binary="dist/build_linux_amd64_v1/$asset"
digest="$(sha256sum "$binary" | cut -d' ' -f1)"
published_digest="$(gh api "repos/$GITHUB_REPOSITORY/releases/tags/$PROVIDER_TAG" \
--jq ".assets[] | select(.name == \"$asset\") | .digest")"
if [ "$published_digest" != "sha256:$digest" ]; then
echo "::error::downloaded provider sha256:$digest differs from '$published_digest'"
exit 1
fi
chmod 0755 "$binary"
echo "provider binary sha256: $digest (matches published release digest)" \
| tee -a "$QUALIFICATION_DIR/pair-verification.txt" >> "$GITHUB_STEP_SUMMARY"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: require the explicit candidate before SSH setup
working-directory: ./e2e
run: |
status=0
env -u DEVSY_E2E_BINARY go test -v -timeout 300s --ginkgo.v \
--ginkgo.label-filter=release-qualification \
--ginkgo.focus='bash builtin=false installs a cold agent by remote download' \
> "$QUALIFICATION_DIR/missing-binary-guard.log" 2>&1 || status=$?
cat "$QUALIFICATION_DIR/missing-binary-guard.log"
test "$status" -eq 1
grep -F 'DEVSY_E2E_BINARY is required' "$QUALIFICATION_DIR/missing-binary-guard.log"
env:
DEVSY_E2E_RELEASE_QUALIFICATION: "true"

- name: provision login shell accounts
run: |
sudo service ssh start
for shell in bash zsh fish; do
sudo useradd --create-home --shell "$(command -v "$shell")" "devsy-$shell"
sudo usermod --append --groups docker "devsy-$shell"
done
sudo useradd --create-home --shell "$(command -v bash)" --groups docker devsy-sudo
policy=/etc/sudoers.d/devsy-e2e-qualification
sudo test ! -e "$policy"
rule="$(mktemp)"
trap 'rm -f "$rule"' EXIT
printf '%s\n' 'devsy-sudo ALL=(root) NOPASSWD: SETENV: /bin/sh -c * devsy-install *' > "$rule"
sudo visudo -cf "$rule"
echo "DEVSY_SUDO_FIXTURE_POLICY=$policy" >> "$GITHUB_ENV"
sudo install -m 0440 -o root -g root "$rule" "$policy"
sudo visudo -cf "$policy"

- name: qualify login shells with the explicit pair
working-directory: ./e2e
run: >-
go test -v -ginkgo.v -timeout 14400s
--ginkgo.label-filter='release-qualification || provider-command'
env:
DEVSY_E2E_RELEASE_QUALIFICATION: "true"
DEVSY_E2E_SHELL_MATRIX: "true"
DEVSY_E2E_REPORT: ${{ runner.temp }}/qualification/qualification-report.json

- name: remove owned sudo fixture policy
if: always() && env.DEVSY_SUDO_FIXTURE_POLICY != ''
run: sudo rm -f -- "$DEVSY_SUDO_FIXTURE_POLICY"

- name: require a complete, passing report
run: |
jq -e --arg sha "$CORE_SHA256" '
.pair.coreBinarySha256 == $sha
and (.cells | length == 6)
and ([.cells[] | [.shell, .builtinSsh]] | unique | length == 6)
and all(.cells[];
.cleanup == "ok"
and (.scenarios | length == 5)
and all(.scenarios[]; .outcome == "passed")
and (.agentDir | contains(" ") and contains("\u0027")
and contains("\\") and contains("$") and contains("\u03bb"))
and .agentSha256.download == $sha
and .agentSha256.stdin == $sha)
and .privilege.user == "devsy-sudo"
and .privilege.userUid > 0
and .privilege.directoryState == "0:755"
and .privilege.writeDenied == true
and .privilege.agentUid == 0
and .privilege.agentVersion == .pair.coreVersion
and .privilege.agentSha256 == $sha
and .privilege.liveCommand == true
and .privilege.outcome == "passed"
and .privilege.cleanup == "ok"
' "$QUALIFICATION_DIR/qualification-report.json"

- name: upload qualification report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: release-qualification-${{ inputs.core_tag }}-${{ matrix.core_artifact }}
path: |
${{ runner.temp }}/qualification/pair-verification.txt
${{ runner.temp }}/qualification/qualification-report.json
${{ runner.temp }}/qualification/missing-binary-guard.log
if-no-files-found: error
60 changes: 60 additions & 0 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,63 @@ go get github.com/onsi/ginkgo/ginkgo
# Install ginkgo and run in this directory
go test -v -ginkgo.v -timeout 3600s
```

#### Release qualification (explicit core/provider pair)

The default lifecycle suite downloads core v1.24.0 unless `DEVSY_E2E_BINARY` is set.
That run checks backward compatibility only and is not release qualification.

The qualification suite requires an explicit `DEVSY_E2E_BINARY`; missing candidate
metadata or host prerequisites fail rather than selecting the compatibility core.
Dispatch the `Tests` workflow with `core_tag` and `provider_tag` (default `v1.1.47`).
Two independent jobs qualify the standalone Linux amd64 CLI and the CLI extracted
from the Desktop candidate's `Devsy_linux_amd64.deb`. Each job verifies the published
asset digest, executes that binary's `--version`, records its SHA256, and checks that
core `e22be00` and provider `d31fed1` are ancestors of the selected release commits.
The provider binary and checksums come from the selected provider release.

The jobs run `release-qualification || provider-command` and upload
`release-qualification-<tag>-cli` and `release-qualification-<tag>-desktop` reports.
The report identifies the exact core/provider pair and harness commit, all six
shell/transport cells, five agent scenarios per cell, binary hashes, and cleanup.
All six cells use an agent path containing a space, apostrophe, backslash, dollar
sign, and Unicode character; every remote helper quotes the path literally.
One additional focused scenario uses the dedicated non-root `devsy-sudo` account
and a root-owned `0755` agent directory. It proves that an ordinary write is
denied, a real sudo installation produces a root-owned candidate agent with the
expected version/checksum, and workspace SSH still executes a live command.
A skipped or incomplete matrix or privilege scenario cannot pass the report gate.

To run by hand, use a disposable Linux amd64 host with local sshd, Docker, and
provisioned `devsy-bash`, `devsy-zsh`, `devsy-fish`, and `devsy-sudo` accounts in the
`docker` group. Use the workflow's provisioning on that owned host: only
`devsy-sudo` receives the validated, temporary sudoers rule for
`/bin/sh -c * devsy-install *`, with `NOPASSWD` and `SETENV` for the installer's
explicit dispatch. Remove `/etc/sudoers.d/devsy-e2e-qualification` after the run,
including failure paths, as the workflow's `always()` cleanup does.
Run from `e2e/`, after placing the released provider binary at
`dist/build_linux_amd64_v1/devsy-provider-ssh-linux-amd64` and its release checksums
at `dist/checksums.txt` relative to the repository root. Unset `DEVSY_AGENT_URL`,
`DEVSY_AGENT_BINARY`, and `DEVSY_AGENT_PREFER_DOWNLOAD`: inherited overrides can
disable the version check or select the wrong agent transport.

```
DEVSY_E2E_RELEASE_QUALIFICATION=true DEVSY_E2E_SHELL_MATRIX=true \
DEVSY_E2E_BINARY=/path/to/devsy-linux-amd64 DEVSY_E2E_CORE_TAG=v1.25.0-beta.3 \
DEVSY_E2E_CORE_SHA=fe22f9a1225d6c1547001755e94c95bb2cdbc8db \
DEVSY_E2E_CORE_VERSION=v1.25.0-beta.3 \
DEVSY_E2E_PROVIDER_TAG=v1.1.47 \
DEVSY_E2E_PROVIDER_SHA=c53ba2742db162f587aa57fbc87572d19ec11cd7 \
go test -v -ginkgo.v -timeout 14400s \
--ginkgo.label-filter='release-qualification || provider-command'
```

Only disposable fixture accounts are provisioned; never change a user's login shell,
`SHELL`, `/etc/passwd`, or `.zshrc` to make qualification pass. Download and stdin
transfer must deliver the candidate's exact bytes. The matrix retains the existing
command-boundary stream, stderr, and nonzero-exit coverage.

Extracting and qualifying the Desktop-bundled CLI is not native Desktop UI
acceptance, installation/update validation, or a customer-host reproduction.
Stable release publication, supported-platform artifact smokes, reporter-equivalent
Desktop fresh/reopen traces, and customer confirmation remain separate release work.
20 changes: 17 additions & 3 deletions e2e/tests/integration/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,16 @@ func setupSSHKeys() {
}
}

// compatCoreVersion is the core release the lifecycle suite downloads when no
// binary override is supplied. It checks backward compatibility only.
const compatCoreVersion = "v1.24.0"

func setupDevsyCLI() {
if local := os.Getenv("DEVSY_E2E_BINARY"); local != "" {
if qualificationEnabled() {
requireQualifiedPair()
}

if local := os.Getenv(envCoreBinary); local != "" {
// #nosec G304,G703 -- Test runner explicitly selects the local core binary.
data, err := os.ReadFile(local)
framework.ExpectNoError(err)
Expand All @@ -83,9 +91,15 @@ func setupDevsyCLI() {
return
}

downloadCompatCore()
}

func downloadCompatCore() {
ginkgo.GinkgoWriter.Printf("compatibility run against core %s\n", compatCoreVersion)
client := &http.Client{Timeout: time.Second * 30}
resp, err := client.Get(
"https://github.com/devsy-org/devsy/releases/download/v1.24.0/devsy-linux-amd64",
"https://github.com/devsy-org/devsy/releases/download/" + compatCoreVersion +
"/devsy-linux-amd64",
)
framework.ExpectNoError(err)
framework.ExpectEqual(resp.StatusCode, http.StatusOK)
Expand Down Expand Up @@ -115,7 +129,7 @@ func setupDevsyCLI() {
}

var _ = ginkgo.Describe(
"devsy provider ssh test suite",
"devsy provider ssh lifecycle suite (compatibility core "+compatCoreVersion+" unless overridden)",
ginkgo.Label("integration"),
ginkgo.Ordered,
func() {
Expand Down
Loading
Loading