Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1276135a3a | ||
|
|
42f06c81ba | ||
|
|
5ed27e7d58 | ||
|
|
5098e02556 | ||
|
|
06c6abbe09 | ||
|
|
db0f0aaa14 | ||
|
|
bef0766929 | ||
|
|
41eed1dd0d | ||
|
|
2cbb86200c | ||
|
|
483f4d5efb | ||
|
|
800dd9cc66 | ||
|
|
035ad33faf | ||
|
|
230b55bfa0 | ||
|
|
b2d6a18861 | ||
|
|
c0cd32c2c4 | ||
|
|
c2414d5287 | ||
|
|
ff69ed69df |
2
.github/actions/setup-go/action.yaml
vendored
2
.github/actions/setup-go/action.yaml
vendored
@@ -4,7 +4,7 @@ description: |
|
||||
inputs:
|
||||
version:
|
||||
description: "The Go version to use."
|
||||
default: "1.24.6"
|
||||
default: "1.24.10"
|
||||
use-preinstalled-go:
|
||||
description: "Whether to use preinstalled Go."
|
||||
default: "false"
|
||||
|
||||
4
.github/dependabot.yaml
vendored
4
.github/dependabot.yaml
vendored
@@ -80,6 +80,9 @@ updates:
|
||||
mui:
|
||||
patterns:
|
||||
- "@mui*"
|
||||
radix:
|
||||
patterns:
|
||||
- "@radix-ui/*"
|
||||
react:
|
||||
patterns:
|
||||
- "react"
|
||||
@@ -104,6 +107,7 @@ updates:
|
||||
- dependency-name: "*"
|
||||
update-types:
|
||||
- version-update:semver-major
|
||||
- dependency-name: "@playwright/test"
|
||||
open-pull-requests-limit: 15
|
||||
|
||||
- package-ecosystem: "terraform"
|
||||
|
||||
154
.github/workflows/ci.yaml
vendored
154
.github/workflows/ci.yaml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- release/*
|
||||
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
@@ -919,6 +920,7 @@ jobs:
|
||||
required:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- changes
|
||||
- fmt
|
||||
- lint
|
||||
- gen
|
||||
@@ -942,6 +944,7 @@ jobs:
|
||||
- name: Ensure required checks
|
||||
run: | # zizmor: ignore[template-injection] We're just reading needs.x.result here, no risk of injection
|
||||
echo "Checking required checks"
|
||||
echo "- changes: ${{ needs.changes.result }}"
|
||||
echo "- fmt: ${{ needs.fmt.result }}"
|
||||
echo "- lint: ${{ needs.lint.result }}"
|
||||
echo "- gen: ${{ needs.gen.result }}"
|
||||
@@ -967,7 +970,7 @@ jobs:
|
||||
needs: changes
|
||||
# We always build the dylibs on Go changes to verify we're not merging unbuildable code,
|
||||
# but they need only be signed and uploaded on coder/coder main.
|
||||
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main'
|
||||
if: needs.changes.outputs.go == 'true' || needs.changes.outputs.ci == 'true' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest' }}
|
||||
steps:
|
||||
# Harden Runner doesn't work on macOS
|
||||
@@ -995,7 +998,7 @@ jobs:
|
||||
uses: ./.github/actions/setup-go
|
||||
|
||||
- name: Install rcodesign
|
||||
if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ github.repository_owner == 'coder' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
wget -O /tmp/rcodesign.tar.gz https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz
|
||||
@@ -1006,7 +1009,7 @@ jobs:
|
||||
rm /tmp/rcodesign.tar.gz
|
||||
|
||||
- name: Setup Apple Developer certificate and API key
|
||||
if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ github.repository_owner == 'coder' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
touch /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
|
||||
@@ -1027,12 +1030,12 @@ jobs:
|
||||
make gen/mark-fresh
|
||||
make build/coder-dylib
|
||||
env:
|
||||
CODER_SIGN_DARWIN: ${{ github.ref == 'refs/heads/main' && '1' || '0' }}
|
||||
CODER_SIGN_DARWIN: ${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && '1' || '0' }}
|
||||
AC_CERTIFICATE_FILE: /tmp/apple_cert.p12
|
||||
AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt
|
||||
|
||||
- name: Upload build artifacts
|
||||
if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ github.repository_owner == 'coder' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: dylibs
|
||||
@@ -1042,7 +1045,7 @@ jobs:
|
||||
retention-days: 7
|
||||
|
||||
- name: Delete Apple Developer certificate and API key
|
||||
if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }}
|
||||
if: ${{ github.repository_owner == 'coder' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) }}
|
||||
run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8}
|
||||
|
||||
check-build:
|
||||
@@ -1092,7 +1095,7 @@ jobs:
|
||||
needs:
|
||||
- changes
|
||||
- build-dylib
|
||||
if: github.ref == 'refs/heads/main' && needs.changes.outputs.docs-only == 'false' && !github.event.pull_request.head.repo.fork
|
||||
if: (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && needs.changes.outputs.docs-only == 'false' && !github.event.pull_request.head.repo.fork
|
||||
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-22.04' }}
|
||||
permissions:
|
||||
# Necessary to push docker images to ghcr.io.
|
||||
@@ -1245,40 +1248,45 @@ jobs:
|
||||
id: build-docker
|
||||
env:
|
||||
CODER_IMAGE_BASE: ghcr.io/coder/coder-preview
|
||||
CODER_IMAGE_TAG_PREFIX: main
|
||||
DOCKER_CLI_EXPERIMENTAL: "enabled"
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
|
||||
# build Docker images for each architecture
|
||||
version="$(./scripts/version.sh)"
|
||||
tag="main-${version//+/-}"
|
||||
tag="${version//+/-}"
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# build images for each architecture
|
||||
# note: omitting the -j argument to avoid race conditions when pushing
|
||||
make build/coder_"$version"_linux_{amd64,arm64,armv7}.tag
|
||||
|
||||
# only push if we are on main branch
|
||||
if [ "${GITHUB_REF}" == "refs/heads/main" ]; then
|
||||
# only push if we are on main branch or release branch
|
||||
if [[ "${GITHUB_REF}" == "refs/heads/main" || "${GITHUB_REF}" == refs/heads/release/* ]]; then
|
||||
# build and push multi-arch manifest, this depends on the other images
|
||||
# being pushed so will automatically push them
|
||||
# note: omitting the -j argument to avoid race conditions when pushing
|
||||
make push/build/coder_"$version"_linux_{amd64,arm64,armv7}.tag
|
||||
|
||||
# Define specific tags
|
||||
tags=("$tag" "main" "latest")
|
||||
tags=("$tag")
|
||||
if [ "${GITHUB_REF}" == "refs/heads/main" ]; then
|
||||
tags+=("main" "latest")
|
||||
elif [[ "${GITHUB_REF}" == refs/heads/release/* ]]; then
|
||||
tags+=("release-${GITHUB_REF#refs/heads/release/}")
|
||||
fi
|
||||
|
||||
# Create and push a multi-arch manifest for each tag
|
||||
# we are adding `latest` tag and keeping `main` for backward
|
||||
# compatibality
|
||||
for t in "${tags[@]}"; do
|
||||
# shellcheck disable=SC2046
|
||||
./scripts/build_docker_multiarch.sh \
|
||||
--push \
|
||||
--target "ghcr.io/coder/coder-preview:$t" \
|
||||
--version "$version" \
|
||||
$(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag)
|
||||
echo "Pushing multi-arch manifest for tag: $t"
|
||||
# shellcheck disable=SC2046
|
||||
./scripts/build_docker_multiarch.sh \
|
||||
--push \
|
||||
--target "ghcr.io/coder/coder-preview:$t" \
|
||||
--version "$version" \
|
||||
$(cat build/coder_"$version"_linux_{amd64,arm64,armv7}.tag)
|
||||
done
|
||||
fi
|
||||
|
||||
@@ -1469,112 +1477,28 @@ jobs:
|
||||
./build/*.deb
|
||||
retention-days: 7
|
||||
|
||||
# Deploy is handled in deploy.yaml so we can apply concurrency limits.
|
||||
deploy:
|
||||
name: "deploy"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
needs:
|
||||
- changes
|
||||
- build
|
||||
if: |
|
||||
github.ref == 'refs/heads/main' && !github.event.pull_request.head.repo.fork
|
||||
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/'))
|
||||
&& needs.changes.outputs.docs-only == 'false'
|
||||
&& !github.event.pull_request.head.repo.fork
|
||||
uses: ./.github/workflows/deploy.yaml
|
||||
with:
|
||||
image: ${{ needs.build.outputs.IMAGE }}
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
|
||||
with:
|
||||
workload_identity_provider: ${{ vars.GCP_WORKLOAD_ID_PROVIDER }}
|
||||
service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}
|
||||
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
|
||||
|
||||
- name: Set up Flux CLI
|
||||
uses: fluxcd/flux2/action@6bf37f6a560fd84982d67f853162e4b3c2235edb # v2.6.4
|
||||
with:
|
||||
# Keep this and the github action up to date with the version of flux installed in dogfood cluster
|
||||
version: "2.5.1"
|
||||
|
||||
- name: Get Cluster Credentials
|
||||
uses: google-github-actions/get-gke-credentials@3da1e46a907576cefaa90c484278bb5b259dd395 # v3.0.0
|
||||
with:
|
||||
cluster_name: dogfood-v2
|
||||
location: us-central1-a
|
||||
project_id: coder-dogfood-v2
|
||||
|
||||
- name: Reconcile Flux
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
flux --namespace flux-system reconcile source git flux-system
|
||||
flux --namespace flux-system reconcile source git coder-main
|
||||
flux --namespace flux-system reconcile kustomization flux-system
|
||||
flux --namespace flux-system reconcile kustomization coder
|
||||
flux --namespace flux-system reconcile source chart coder-coder
|
||||
flux --namespace flux-system reconcile source chart coder-coder-provisioner
|
||||
flux --namespace coder reconcile helmrelease coder
|
||||
flux --namespace coder reconcile helmrelease coder-provisioner
|
||||
|
||||
# Just updating Flux is usually not enough. The Helm release may get
|
||||
# redeployed, but unless something causes the Deployment to update the
|
||||
# pods won't be recreated. It's important that the pods get recreated,
|
||||
# since we use `imagePullPolicy: Always` to ensure we're running the
|
||||
# latest image.
|
||||
- name: Rollout Deployment
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
kubectl --namespace coder rollout restart deployment/coder
|
||||
kubectl --namespace coder rollout status deployment/coder
|
||||
kubectl --namespace coder rollout restart deployment/coder-provisioner
|
||||
kubectl --namespace coder rollout status deployment/coder-provisioner
|
||||
kubectl --namespace coder rollout restart deployment/coder-provisioner-tagged
|
||||
kubectl --namespace coder rollout status deployment/coder-provisioner-tagged
|
||||
|
||||
deploy-wsproxies:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: github.ref == 'refs/heads/main' && !github.event.pull_request.head.repo.fork
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup flyctl
|
||||
uses: superfly/flyctl-actions/setup-flyctl@fc53c09e1bc3be6f54706524e3b82c4f462f77be # v1.5
|
||||
|
||||
- name: Deploy workspace proxies
|
||||
run: |
|
||||
flyctl deploy --image "$IMAGE" --app paris-coder --config ./.github/fly-wsproxies/paris-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_PARIS" --yes
|
||||
flyctl deploy --image "$IMAGE" --app sydney-coder --config ./.github/fly-wsproxies/sydney-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_SYDNEY" --yes
|
||||
flyctl deploy --image "$IMAGE" --app sao-paulo-coder --config ./.github/fly-wsproxies/sao-paulo-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_SAO_PAULO" --yes
|
||||
flyctl deploy --image "$IMAGE" --app jnb-coder --config ./.github/fly-wsproxies/jnb-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_JNB" --yes
|
||||
env:
|
||||
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
||||
IMAGE: ${{ needs.build.outputs.IMAGE }}
|
||||
TOKEN_PARIS: ${{ secrets.FLY_PARIS_CODER_PROXY_SESSION_TOKEN }}
|
||||
TOKEN_SYDNEY: ${{ secrets.FLY_SYDNEY_CODER_PROXY_SESSION_TOKEN }}
|
||||
TOKEN_SAO_PAULO: ${{ secrets.FLY_SAO_PAULO_CODER_PROXY_SESSION_TOKEN }}
|
||||
TOKEN_JNB: ${{ secrets.FLY_JNB_CODER_PROXY_SESSION_TOKEN }}
|
||||
packages: write # to retag image as dogfood
|
||||
secrets:
|
||||
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
||||
FLY_PARIS_CODER_PROXY_SESSION_TOKEN: ${{ secrets.FLY_PARIS_CODER_PROXY_SESSION_TOKEN }}
|
||||
FLY_SYDNEY_CODER_PROXY_SESSION_TOKEN: ${{ secrets.FLY_SYDNEY_CODER_PROXY_SESSION_TOKEN }}
|
||||
FLY_SAO_PAULO_CODER_PROXY_SESSION_TOKEN: ${{ secrets.FLY_SAO_PAULO_CODER_PROXY_SESSION_TOKEN }}
|
||||
FLY_JNB_CODER_PROXY_SESSION_TOKEN: ${{ secrets.FLY_JNB_CODER_PROXY_SESSION_TOKEN }}
|
||||
|
||||
# sqlc-vet runs a postgres docker container, runs Coder migrations, and then
|
||||
# runs sqlc-vet to ensure all queries are valid. This catches any mistakes
|
||||
|
||||
170
.github/workflows/deploy.yaml
vendored
Normal file
170
.github/workflows/deploy.yaml
vendored
Normal file
@@ -0,0 +1,170 @@
|
||||
name: deploy
|
||||
|
||||
on:
|
||||
# Via workflow_call, called from ci.yaml
|
||||
workflow_call:
|
||||
inputs:
|
||||
image:
|
||||
description: "Image and tag to potentially deploy. Current branch will be validated against should-deploy check."
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
FLY_API_TOKEN:
|
||||
required: true
|
||||
FLY_PARIS_CODER_PROXY_SESSION_TOKEN:
|
||||
required: true
|
||||
FLY_SYDNEY_CODER_PROXY_SESSION_TOKEN:
|
||||
required: true
|
||||
FLY_SAO_PAULO_CODER_PROXY_SESSION_TOKEN:
|
||||
required: true
|
||||
FLY_JNB_CODER_PROXY_SESSION_TOKEN:
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }} # no per-branch concurrency
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Determines if the given branch should be deployed to dogfood.
|
||||
should-deploy:
|
||||
name: should-deploy
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
verdict: ${{ steps.check.outputs.verdict }} # DEPLOY or NOOP
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check if deploy is enabled
|
||||
id: check
|
||||
run: |
|
||||
set -euo pipefail
|
||||
verdict="$(./scripts/should_deploy.sh)"
|
||||
echo "verdict=$verdict" >> "$GITHUB_OUTPUT"
|
||||
|
||||
deploy:
|
||||
name: "deploy"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
needs: should-deploy
|
||||
if: needs.should-deploy.outputs.verdict == 'DEPLOY'
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
packages: write # to retag image as dogfood
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: GHCR Login
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
|
||||
with:
|
||||
workload_identity_provider: ${{ vars.GCP_WORKLOAD_ID_PROVIDER }}
|
||||
service_account: ${{ vars.GCP_SERVICE_ACCOUNT }}
|
||||
|
||||
- name: Set up Google Cloud SDK
|
||||
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
|
||||
|
||||
- name: Set up Flux CLI
|
||||
uses: fluxcd/flux2/action@6bf37f6a560fd84982d67f853162e4b3c2235edb # v2.6.4
|
||||
with:
|
||||
# Keep this and the github action up to date with the version of flux installed in dogfood cluster
|
||||
version: "2.7.0"
|
||||
|
||||
- name: Get Cluster Credentials
|
||||
uses: google-github-actions/get-gke-credentials@3da1e46a907576cefaa90c484278bb5b259dd395 # v3.0.0
|
||||
with:
|
||||
cluster_name: dogfood-v2
|
||||
location: us-central1-a
|
||||
project_id: coder-dogfood-v2
|
||||
|
||||
# Retag image as dogfood while maintaining the multi-arch manifest
|
||||
- name: Tag image as dogfood
|
||||
run: docker buildx imagetools create --tag "ghcr.io/coder/coder-preview:dogfood" "$IMAGE"
|
||||
env:
|
||||
IMAGE: ${{ inputs.image }}
|
||||
|
||||
- name: Reconcile Flux
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
flux --namespace flux-system reconcile source git flux-system
|
||||
flux --namespace flux-system reconcile source git coder-main
|
||||
flux --namespace flux-system reconcile kustomization flux-system
|
||||
flux --namespace flux-system reconcile kustomization coder
|
||||
flux --namespace flux-system reconcile source chart coder-coder
|
||||
flux --namespace flux-system reconcile source chart coder-coder-provisioner
|
||||
flux --namespace coder reconcile helmrelease coder
|
||||
flux --namespace coder reconcile helmrelease coder-provisioner
|
||||
|
||||
# Just updating Flux is usually not enough. The Helm release may get
|
||||
# redeployed, but unless something causes the Deployment to update the
|
||||
# pods won't be recreated. It's important that the pods get recreated,
|
||||
# since we use `imagePullPolicy: Always` to ensure we're running the
|
||||
# latest image.
|
||||
- name: Rollout Deployment
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
kubectl --namespace coder rollout restart deployment/coder
|
||||
kubectl --namespace coder rollout status deployment/coder
|
||||
kubectl --namespace coder rollout restart deployment/coder-provisioner
|
||||
kubectl --namespace coder rollout status deployment/coder-provisioner
|
||||
kubectl --namespace coder rollout restart deployment/coder-provisioner-tagged
|
||||
kubectl --namespace coder rollout status deployment/coder-provisioner-tagged
|
||||
|
||||
deploy-wsproxies:
|
||||
runs-on: ubuntu-latest
|
||||
needs: deploy
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- name: Setup flyctl
|
||||
uses: superfly/flyctl-actions/setup-flyctl@fc53c09e1bc3be6f54706524e3b82c4f462f77be # v1.5
|
||||
|
||||
- name: Deploy workspace proxies
|
||||
run: |
|
||||
flyctl deploy --image "$IMAGE" --app paris-coder --config ./.github/fly-wsproxies/paris-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_PARIS" --yes
|
||||
flyctl deploy --image "$IMAGE" --app sydney-coder --config ./.github/fly-wsproxies/sydney-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_SYDNEY" --yes
|
||||
flyctl deploy --image "$IMAGE" --app sao-paulo-coder --config ./.github/fly-wsproxies/sao-paulo-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_SAO_PAULO" --yes
|
||||
flyctl deploy --image "$IMAGE" --app jnb-coder --config ./.github/fly-wsproxies/jnb-coder.toml --env "CODER_PROXY_SESSION_TOKEN=$TOKEN_JNB" --yes
|
||||
env:
|
||||
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
|
||||
IMAGE: ${{ inputs.image }}
|
||||
TOKEN_PARIS: ${{ secrets.FLY_PARIS_CODER_PROXY_SESSION_TOKEN }}
|
||||
TOKEN_SYDNEY: ${{ secrets.FLY_SYDNEY_CODER_PROXY_SESSION_TOKEN }}
|
||||
TOKEN_SAO_PAULO: ${{ secrets.FLY_SAO_PAULO_CODER_PROXY_SESSION_TOKEN }}
|
||||
TOKEN_JNB: ${{ secrets.FLY_JNB_CODER_PROXY_SESSION_TOKEN }}
|
||||
4
.github/zizmor.yml
vendored
Normal file
4
.github/zizmor.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
rules:
|
||||
cache-poisoning:
|
||||
ignore:
|
||||
- "ci.yaml:184"
|
||||
@@ -1076,7 +1076,7 @@ func (a *agent) handleManifest(manifestOK *checkpoint) func(ctx context.Context,
|
||||
if err != nil {
|
||||
return xerrors.Errorf("fetch metadata: %w", err)
|
||||
}
|
||||
a.logger.Info(ctx, "fetched manifest", slog.F("manifest", mp))
|
||||
a.logger.Info(ctx, "fetched manifest")
|
||||
manifest, err := agentsdk.ManifestFromProto(mp)
|
||||
if err != nil {
|
||||
a.logger.Critical(ctx, "failed to convert manifest", slog.F("manifest", mp), slog.Error(err))
|
||||
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -977,6 +979,7 @@ func TestTasksNotification(t *testing.T) {
|
||||
isAITask bool
|
||||
isNotificationSent bool
|
||||
notificationTemplate uuid.UUID
|
||||
taskPrompt string
|
||||
}{
|
||||
// Should not send a notification when the agent app is not an AI task.
|
||||
{
|
||||
@@ -985,6 +988,7 @@ func TestTasksNotification(t *testing.T) {
|
||||
newAppStatus: codersdk.WorkspaceAppStatusStateWorking,
|
||||
isAITask: false,
|
||||
isNotificationSent: false,
|
||||
taskPrompt: "NoAITask",
|
||||
},
|
||||
// Should not send a notification when the new app status is neither 'Working' nor 'Idle'.
|
||||
{
|
||||
@@ -993,6 +997,7 @@ func TestTasksNotification(t *testing.T) {
|
||||
newAppStatus: codersdk.WorkspaceAppStatusStateComplete,
|
||||
isAITask: true,
|
||||
isNotificationSent: false,
|
||||
taskPrompt: "NonNotifiedState",
|
||||
},
|
||||
// Should not send a notification when the new app status equals the latest status (Working).
|
||||
{
|
||||
@@ -1001,6 +1006,7 @@ func TestTasksNotification(t *testing.T) {
|
||||
newAppStatus: codersdk.WorkspaceAppStatusStateWorking,
|
||||
isAITask: true,
|
||||
isNotificationSent: false,
|
||||
taskPrompt: "NonNotifiedTransition",
|
||||
},
|
||||
// Should send TemplateTaskWorking when the AI task transitions to 'Working'.
|
||||
{
|
||||
@@ -1010,6 +1016,7 @@ func TestTasksNotification(t *testing.T) {
|
||||
isAITask: true,
|
||||
isNotificationSent: true,
|
||||
notificationTemplate: notifications.TemplateTaskWorking,
|
||||
taskPrompt: "TemplateTaskWorking",
|
||||
},
|
||||
// Should send TemplateTaskWorking when the AI task transitions to 'Working' from 'Idle'.
|
||||
{
|
||||
@@ -1022,6 +1029,7 @@ func TestTasksNotification(t *testing.T) {
|
||||
isAITask: true,
|
||||
isNotificationSent: true,
|
||||
notificationTemplate: notifications.TemplateTaskWorking,
|
||||
taskPrompt: "TemplateTaskWorkingFromIdle",
|
||||
},
|
||||
// Should send TemplateTaskIdle when the AI task transitions to 'Idle'.
|
||||
{
|
||||
@@ -1031,6 +1039,17 @@ func TestTasksNotification(t *testing.T) {
|
||||
isAITask: true,
|
||||
isNotificationSent: true,
|
||||
notificationTemplate: notifications.TemplateTaskIdle,
|
||||
taskPrompt: "TemplateTaskIdle",
|
||||
},
|
||||
// Long task prompts should be truncated to 160 characters.
|
||||
{
|
||||
name: "LongTaskPrompt",
|
||||
latestAppStatuses: []codersdk.WorkspaceAppStatusState{codersdk.WorkspaceAppStatusStateWorking},
|
||||
newAppStatus: codersdk.WorkspaceAppStatusStateIdle,
|
||||
isAITask: true,
|
||||
isNotificationSent: true,
|
||||
notificationTemplate: notifications.TemplateTaskIdle,
|
||||
taskPrompt: "This is a very long task prompt that should be truncated to 160 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -1067,7 +1086,7 @@ func TestTasksNotification(t *testing.T) {
|
||||
}).Seed(workspaceBuildSeed).Params(database.WorkspaceBuildParameter{
|
||||
WorkspaceBuildID: workspaceBuildID,
|
||||
Name: codersdk.AITaskPromptParameterName,
|
||||
Value: "task prompt",
|
||||
Value: tc.taskPrompt,
|
||||
}).WithAgent(func(agent []*proto.Agent) []*proto.Agent {
|
||||
agent[0].Apps = []*proto.App{{
|
||||
Id: workspaceAgentAppID.String(),
|
||||
@@ -1115,7 +1134,13 @@ func TestTasksNotification(t *testing.T) {
|
||||
require.Len(t, sent, 1)
|
||||
require.Equal(t, memberUser.ID, sent[0].UserID)
|
||||
require.Len(t, sent[0].Labels, 2)
|
||||
require.Equal(t, "task prompt", sent[0].Labels["task"])
|
||||
// NOTE: len(string) is the number of bytes in the string, not the number of runes.
|
||||
require.LessOrEqual(t, utf8.RuneCountInString(sent[0].Labels["task"]), 160)
|
||||
if len(tc.taskPrompt) > 160 {
|
||||
require.Contains(t, tc.taskPrompt, strings.TrimSuffix(sent[0].Labels["task"], "…"))
|
||||
} else {
|
||||
require.Equal(t, tc.taskPrompt, sent[0].Labels["task"])
|
||||
}
|
||||
require.Equal(t, workspace.Name, sent[0].Labels["workspace"])
|
||||
} else {
|
||||
// Then: No notification is sent
|
||||
|
||||
@@ -217,7 +217,7 @@ var (
|
||||
rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate},
|
||||
// Unsure why provisionerd needs update and read personal
|
||||
rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal},
|
||||
rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop},
|
||||
rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent},
|
||||
rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionCreateAgent},
|
||||
rbac.ResourceApiKey.Type: {policy.WildcardSymbol},
|
||||
// When org scoped provisioner credentials are implemented,
|
||||
|
||||
@@ -141,13 +141,19 @@ ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_proxy:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'workspace_proxy:update';
|
||||
-- End enum extensions
|
||||
|
||||
-- Purge old API keys to speed up the migration for large deployments.
|
||||
-- Note: that problem should be solved in coderd once PR 20863 is released:
|
||||
-- https://github.com/coder/coder/blob/main/coderd/database/dbpurge/dbpurge.go#L85
|
||||
DELETE FROM api_keys WHERE expires_at < NOW() - INTERVAL '7 days';
|
||||
|
||||
-- Add new columns without defaults; backfill; then enforce NOT NULL
|
||||
ALTER TABLE api_keys ADD COLUMN scopes api_key_scope[];
|
||||
ALTER TABLE api_keys ADD COLUMN allow_list text[];
|
||||
|
||||
-- Backfill existing rows for compatibility
|
||||
UPDATE api_keys SET scopes = ARRAY[scope::api_key_scope];
|
||||
UPDATE api_keys SET allow_list = ARRAY['*:*'];
|
||||
UPDATE api_keys SET
|
||||
scopes = ARRAY[scope::api_key_scope],
|
||||
allow_list = ARRAY['*:*'];
|
||||
|
||||
-- Enforce NOT NULL
|
||||
ALTER TABLE api_keys ALTER COLUMN scopes SET NOT NULL;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Ensure api_keys and oauth2_provider_app_tokens have live data after
|
||||
-- migration 000371 deletes expired rows.
|
||||
INSERT INTO api_keys (
|
||||
id,
|
||||
hashed_secret,
|
||||
user_id,
|
||||
last_used,
|
||||
expires_at,
|
||||
created_at,
|
||||
updated_at,
|
||||
login_type,
|
||||
lifetime_seconds,
|
||||
ip_address,
|
||||
token_name,
|
||||
scopes,
|
||||
allow_list
|
||||
)
|
||||
VALUES (
|
||||
'fixture-api-key',
|
||||
'\xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
||||
'30095c71-380b-457a-8995-97b8ee6e5307',
|
||||
NOW() - INTERVAL '1 hour',
|
||||
NOW() + INTERVAL '30 days',
|
||||
NOW() - INTERVAL '1 day',
|
||||
NOW() - INTERVAL '1 day',
|
||||
'password',
|
||||
86400,
|
||||
'0.0.0.0',
|
||||
'fixture-api-key',
|
||||
ARRAY['workspace:read']::api_key_scope[],
|
||||
ARRAY['*:*']
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
INSERT INTO oauth2_provider_app_tokens (
|
||||
id,
|
||||
created_at,
|
||||
expires_at,
|
||||
hash_prefix,
|
||||
refresh_hash,
|
||||
app_secret_id,
|
||||
api_key_id,
|
||||
audience,
|
||||
user_id
|
||||
)
|
||||
VALUES (
|
||||
'9f92f3c9-811f-4f6f-9a1c-3f2eed1f9f15',
|
||||
NOW() - INTERVAL '30 minutes',
|
||||
NOW() + INTERVAL '30 days',
|
||||
CAST('fixture-hash-prefix' AS bytea),
|
||||
CAST('fixture-refresh-hash' AS bytea),
|
||||
'b0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11',
|
||||
'fixture-api-key',
|
||||
'https://coder.example.com',
|
||||
'30095c71-380b-457a-8995-97b8ee6e5307'
|
||||
)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
@@ -22533,6 +22533,7 @@ SET
|
||||
WHERE
|
||||
template_id = $3
|
||||
AND dormant_at IS NOT NULL
|
||||
AND deleted = false
|
||||
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
|
||||
-- should not have their dormant or deleting at set, as these are handled by the
|
||||
-- prebuilds reconciliation loop.
|
||||
|
||||
@@ -853,6 +853,7 @@ SET
|
||||
WHERE
|
||||
template_id = @template_id
|
||||
AND dormant_at IS NOT NULL
|
||||
AND deleted = false
|
||||
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
|
||||
-- should not have their dormant or deleting at set, as these are handled by the
|
||||
-- prebuilds reconciliation loop.
|
||||
|
||||
@@ -23,15 +23,64 @@ func JoinWithConjunction(s []string) string {
|
||||
)
|
||||
}
|
||||
|
||||
// Truncate returns the first n characters of s.
|
||||
func Truncate(s string, n int) string {
|
||||
type TruncateOption int
|
||||
|
||||
func (o TruncateOption) String() string {
|
||||
switch o {
|
||||
case TruncateWithEllipsis:
|
||||
return "TruncateWithEllipsis"
|
||||
case TruncateWithFullWords:
|
||||
return "TruncateWithFullWords"
|
||||
default:
|
||||
return fmt.Sprintf("TruncateOption(%d)", o)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
// TruncateWithEllipsis adds a Unicode ellipsis character to the end of the string.
|
||||
TruncateWithEllipsis TruncateOption = 1 << 0
|
||||
// TruncateWithFullWords ensures that words are not split in the middle.
|
||||
// As a special case, if there is no word boundary, the string is truncated.
|
||||
TruncateWithFullWords TruncateOption = 1 << 1
|
||||
)
|
||||
|
||||
// Truncate truncates s to n characters.
|
||||
// Additional behaviors can be specified using TruncateOptions.
|
||||
func Truncate(s string, n int, opts ...TruncateOption) string {
|
||||
var options TruncateOption
|
||||
for _, opt := range opts {
|
||||
options |= opt
|
||||
}
|
||||
if n < 1 {
|
||||
return ""
|
||||
}
|
||||
if len(s) <= n {
|
||||
return s
|
||||
}
|
||||
return s[:n]
|
||||
|
||||
maxLen := n
|
||||
if options&TruncateWithEllipsis != 0 {
|
||||
maxLen--
|
||||
}
|
||||
var sb strings.Builder
|
||||
// If we need to truncate to full words, find the last word boundary before n.
|
||||
if options&TruncateWithFullWords != 0 {
|
||||
lastWordBoundary := strings.LastIndexFunc(s[:maxLen], unicode.IsSpace)
|
||||
if lastWordBoundary < 0 {
|
||||
// We cannot find a word boundary. At this point, we'll truncate the string.
|
||||
// It's better than nothing.
|
||||
_, _ = sb.WriteString(s[:maxLen])
|
||||
} else { // lastWordBoundary <= maxLen
|
||||
_, _ = sb.WriteString(s[:lastWordBoundary])
|
||||
}
|
||||
} else {
|
||||
_, _ = sb.WriteString(s[:maxLen])
|
||||
}
|
||||
|
||||
if options&TruncateWithEllipsis != 0 {
|
||||
_, _ = sb.WriteString("…")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
var bmPolicy = bluemonday.StrictPolicy()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package strings_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -23,17 +24,47 @@ func TestTruncate(t *testing.T) {
|
||||
s string
|
||||
n int
|
||||
expected string
|
||||
options []strings.TruncateOption
|
||||
}{
|
||||
{"foo", 4, "foo"},
|
||||
{"foo", 3, "foo"},
|
||||
{"foo", 2, "fo"},
|
||||
{"foo", 1, "f"},
|
||||
{"foo", 0, ""},
|
||||
{"foo", -1, ""},
|
||||
{"foo", 4, "foo", nil},
|
||||
{"foo", 3, "foo", nil},
|
||||
{"foo", 2, "fo", nil},
|
||||
{"foo", 1, "f", nil},
|
||||
{"foo", 0, "", nil},
|
||||
{"foo", -1, "", nil},
|
||||
{"foo bar", 7, "foo bar", []strings.TruncateOption{strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 6, "foo b…", []strings.TruncateOption{strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 5, "foo …", []strings.TruncateOption{strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 4, "foo…", []strings.TruncateOption{strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 3, "fo…", []strings.TruncateOption{strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 2, "f…", []strings.TruncateOption{strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 1, "…", []strings.TruncateOption{strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 0, "", []strings.TruncateOption{strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 7, "foo bar", []strings.TruncateOption{strings.TruncateWithFullWords}},
|
||||
{"foo bar", 6, "foo", []strings.TruncateOption{strings.TruncateWithFullWords}},
|
||||
{"foo bar", 5, "foo", []strings.TruncateOption{strings.TruncateWithFullWords}},
|
||||
{"foo bar", 4, "foo", []strings.TruncateOption{strings.TruncateWithFullWords}},
|
||||
{"foo bar", 3, "foo", []strings.TruncateOption{strings.TruncateWithFullWords}},
|
||||
{"foo bar", 2, "fo", []strings.TruncateOption{strings.TruncateWithFullWords}},
|
||||
{"foo bar", 1, "f", []strings.TruncateOption{strings.TruncateWithFullWords}},
|
||||
{"foo bar", 0, "", []strings.TruncateOption{strings.TruncateWithFullWords}},
|
||||
{"foo bar", 7, "foo bar", []strings.TruncateOption{strings.TruncateWithFullWords, strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 6, "foo…", []strings.TruncateOption{strings.TruncateWithFullWords, strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 5, "foo…", []strings.TruncateOption{strings.TruncateWithFullWords, strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 4, "foo…", []strings.TruncateOption{strings.TruncateWithFullWords, strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 3, "fo…", []strings.TruncateOption{strings.TruncateWithFullWords, strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 2, "f…", []strings.TruncateOption{strings.TruncateWithFullWords, strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 1, "…", []strings.TruncateOption{strings.TruncateWithFullWords, strings.TruncateWithEllipsis}},
|
||||
{"foo bar", 0, "", []strings.TruncateOption{strings.TruncateWithFullWords, strings.TruncateWithEllipsis}},
|
||||
{"This is a very long task prompt that should be truncated to 160 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", 160, "This is a very long task prompt that should be truncated to 160 characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor…", []strings.TruncateOption{strings.TruncateWithFullWords, strings.TruncateWithEllipsis}},
|
||||
} {
|
||||
t.Run(tt.expected, func(t *testing.T) {
|
||||
tName := fmt.Sprintf("%s_%d", tt.s, tt.n)
|
||||
for _, opt := range tt.options {
|
||||
tName += fmt.Sprintf("_%v", opt)
|
||||
}
|
||||
t.Run(tName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := strings.Truncate(tt.s, tt.n)
|
||||
actual := strings.Truncate(tt.s, tt.n, tt.options...)
|
||||
require.Equal(t, tt.expected, actual)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -484,6 +484,11 @@ func (api *API) enqueueAITaskStateNotification(
|
||||
}
|
||||
}
|
||||
|
||||
// As task prompt may be particularly long, truncate it to 160 characters for notifications.
|
||||
if len(taskName) > 160 {
|
||||
taskName = strutil.Truncate(taskName, 160, strutil.TruncateWithEllipsis, strutil.TruncateWithFullWords)
|
||||
}
|
||||
|
||||
if _, err := api.NotificationsEnqueuer.EnqueueWithData(
|
||||
// nolint:gocritic // Need notifier actor to enqueue notifications
|
||||
dbauthz.AsNotifier(ctx),
|
||||
|
||||
@@ -391,7 +391,7 @@ func (i *InstanceIdentitySessionTokenProvider) GetSessionToken() string {
|
||||
defer cancel()
|
||||
resp, err := i.TokenExchanger.exchange(ctx)
|
||||
if err != nil {
|
||||
i.logger.Error(ctx, "failed to exchange session token: %v", err)
|
||||
i.logger.Error(ctx, "failed to exchange session token", slog.Error(err))
|
||||
return ""
|
||||
}
|
||||
i.sessionToken = resp.SessionToken
|
||||
|
||||
@@ -11,8 +11,8 @@ RUN cargo install jj-cli typos-cli watchexec-cli
|
||||
FROM ubuntu:jammy@sha256:0e5e4a57c2499249aafc3b40fcd541e9a456aab7296681a3994d631587203f97 AS go
|
||||
|
||||
# Install Go manually, so that we can control the version
|
||||
ARG GO_VERSION=1.24.6
|
||||
ARG GO_CHECKSUM="bbca37cc395c974ffa4893ee35819ad23ebb27426df87af92e93a9ec66ef8712"
|
||||
ARG GO_VERSION=1.24.10
|
||||
ARG GO_CHECKSUM="dd52b974e3d9c5a7bbfb222c685806def6be5d6f7efd10f9caa9ca1fa2f47955"
|
||||
|
||||
# Boring Go is needed to build FIPS-compliant binaries.
|
||||
RUN apt-get update && \
|
||||
|
||||
@@ -737,6 +737,105 @@ func TestNotifications(t *testing.T) {
|
||||
require.Contains(t, sent[i].Targets, dormantWs.OwnerID)
|
||||
}
|
||||
})
|
||||
|
||||
// Regression test for https://github.com/coder/coder/issues/20913
|
||||
// Deleted workspaces should not receive dormancy notifications.
|
||||
t.Run("DeletedWorkspacesNotNotified", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
db, _ = dbtestutil.NewDB(t)
|
||||
ctx = testutil.Context(t, testutil.WaitLong)
|
||||
user = dbgen.User(t, db, database.User{})
|
||||
file = dbgen.File(t, db, database.File{
|
||||
CreatedBy: user.ID,
|
||||
})
|
||||
templateJob = dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{
|
||||
FileID: file.ID,
|
||||
InitiatorID: user.ID,
|
||||
Tags: database.StringMap{
|
||||
"foo": "bar",
|
||||
},
|
||||
})
|
||||
timeTilDormant = time.Minute * 2
|
||||
templateVersion = dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
||||
CreatedBy: user.ID,
|
||||
JobID: templateJob.ID,
|
||||
OrganizationID: templateJob.OrganizationID,
|
||||
})
|
||||
template = dbgen.Template(t, db, database.Template{
|
||||
ActiveVersionID: templateVersion.ID,
|
||||
CreatedBy: user.ID,
|
||||
OrganizationID: templateJob.OrganizationID,
|
||||
TimeTilDormant: int64(timeTilDormant),
|
||||
TimeTilDormantAutoDelete: int64(timeTilDormant),
|
||||
})
|
||||
)
|
||||
|
||||
// Create a dormant workspace that is NOT deleted.
|
||||
activeDormantWorkspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OwnerID: user.ID,
|
||||
TemplateID: template.ID,
|
||||
OrganizationID: templateJob.OrganizationID,
|
||||
LastUsedAt: time.Now().Add(-time.Hour),
|
||||
})
|
||||
_, err := db.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{
|
||||
ID: activeDormantWorkspace.ID,
|
||||
DormantAt: sql.NullTime{
|
||||
Time: activeDormantWorkspace.LastUsedAt.Add(timeTilDormant),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create a dormant workspace that IS deleted.
|
||||
deletedDormantWorkspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||||
OwnerID: user.ID,
|
||||
TemplateID: template.ID,
|
||||
OrganizationID: templateJob.OrganizationID,
|
||||
LastUsedAt: time.Now().Add(-time.Hour),
|
||||
Deleted: true, // Mark as deleted
|
||||
})
|
||||
_, err = db.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{
|
||||
ID: deletedDormantWorkspace.ID,
|
||||
DormantAt: sql.NullTime{
|
||||
Time: deletedDormantWorkspace.LastUsedAt.Add(timeTilDormant),
|
||||
Valid: true,
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Setup dependencies
|
||||
notifyEnq := notificationstest.NewFakeEnqueuer()
|
||||
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
const userQuietHoursSchedule = "CRON_TZ=UTC 0 0 * * *" // midnight UTC
|
||||
userQuietHoursStore, err := schedule.NewEnterpriseUserQuietHoursScheduleStore(userQuietHoursSchedule, true)
|
||||
require.NoError(t, err)
|
||||
userQuietHoursStorePtr := &atomic.Pointer[agplschedule.UserQuietHoursScheduleStore]{}
|
||||
userQuietHoursStorePtr.Store(&userQuietHoursStore)
|
||||
templateScheduleStore := schedule.NewEnterpriseTemplateScheduleStore(userQuietHoursStorePtr, notifyEnq, logger, nil)
|
||||
|
||||
// Lower the dormancy TTL to ensure the schedule recalculates deadlines and
|
||||
// triggers notifications.
|
||||
_, err = templateScheduleStore.Set(dbauthz.AsNotifier(ctx), db, template, agplschedule.TemplateScheduleOptions{
|
||||
TimeTilDormant: timeTilDormant / 2,
|
||||
TimeTilDormantAutoDelete: timeTilDormant / 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// We should only receive a notification for the non-deleted dormant workspace.
|
||||
sent := notifyEnq.Sent()
|
||||
require.Len(t, sent, 1, "expected exactly 1 notification for the non-deleted workspace")
|
||||
require.Equal(t, sent[0].UserID, activeDormantWorkspace.OwnerID)
|
||||
require.Equal(t, sent[0].TemplateID, notifications.TemplateWorkspaceMarkedForDeletion)
|
||||
require.Contains(t, sent[0].Targets, activeDormantWorkspace.ID)
|
||||
|
||||
// Ensure the deleted workspace was NOT notified
|
||||
for _, notification := range sent {
|
||||
require.NotContains(t, notification.Targets, deletedDormantWorkspace.ID,
|
||||
"deleted workspace should not receive notifications")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateTTL(t *testing.T) {
|
||||
|
||||
@@ -837,6 +837,73 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
require.True(t, ws.LastUsedAt.After(dormantLastUsedAt))
|
||||
})
|
||||
|
||||
// This test has been added to ensure we don't introduce a regression
|
||||
// to this issue https://github.com/coder/coder/issues/20711.
|
||||
t.Run("DormantAutostop", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
ticker = make(chan time.Time)
|
||||
statCh = make(chan autobuild.Stats)
|
||||
inactiveTTL = time.Minute
|
||||
logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
||||
)
|
||||
|
||||
client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
AutobuildTicker: ticker,
|
||||
AutobuildStats: statCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
},
|
||||
})
|
||||
|
||||
// Create a template version that includes agents on both start AND stop builds.
|
||||
// This simulates a template without `count = data.coder_workspace.me.start_count`.
|
||||
authToken := uuid.NewString()
|
||||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: echo.PlanComplete,
|
||||
ProvisionApply: echo.ProvisionApplyWithAgent(authToken),
|
||||
})
|
||||
|
||||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
|
||||
})
|
||||
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
ws := coderdtest.CreateWorkspace(t, client, template.ID)
|
||||
build := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||||
|
||||
// Simulate the workspace becoming inactive and transitioning to dormant.
|
||||
tickTime := ws.LastUsedAt.Add(inactiveTTL * 2)
|
||||
|
||||
p, err := coderdtest.GetProvisionerForTags(db, time.Now(), ws.OrganizationID, nil)
|
||||
require.NoError(t, err)
|
||||
coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime)
|
||||
ticker <- tickTime
|
||||
stats := <-statCh
|
||||
|
||||
// Expect workspace to transition to stopped state.
|
||||
require.Len(t, stats.Transitions, 1)
|
||||
require.Equal(t, stats.Transitions[ws.ID], database.WorkspaceTransitionStop)
|
||||
|
||||
// The autostop build should succeed even though the template includes
|
||||
// agents without `count = data.coder_workspace.me.start_count`.
|
||||
// This verifies that provisionerd has permission to create agents on
|
||||
// dormant workspaces during stop builds.
|
||||
ws = coderdtest.MustWorkspace(t, client, ws.ID)
|
||||
require.NotNil(t, ws.DormantAt, "workspace should be marked as dormant")
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStop, ws.LatestBuild.Transition)
|
||||
|
||||
latestBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||||
require.Equal(t, codersdk.WorkspaceStatusStopped, latestBuild.Status)
|
||||
})
|
||||
|
||||
// This test serves as a regression prevention for generating
|
||||
// audit logs in the same transaction the transition workspaces to
|
||||
// the dormant state. The auditor that is passed to autobuild does
|
||||
|
||||
6
go.mod
6
go.mod
@@ -1,6 +1,6 @@
|
||||
module github.com/coder/coder/v2
|
||||
|
||||
go 1.24.6
|
||||
go 1.24.10
|
||||
|
||||
// Required until a v3 of chroma is created to lazily initialize all XML files.
|
||||
// None of our dependencies seem to use the registries anyways, so this
|
||||
@@ -462,7 +462,7 @@ require (
|
||||
sigs.k8s.io/yaml v1.5.0 // indirect
|
||||
)
|
||||
|
||||
require github.com/coder/clistat v1.0.0
|
||||
require github.com/coder/clistat v1.1.1
|
||||
|
||||
require github.com/SherClockHolmes/webpush-go v1.4.0
|
||||
|
||||
@@ -478,7 +478,7 @@ require (
|
||||
github.com/anthropics/anthropic-sdk-go v1.12.0
|
||||
github.com/brianvoe/gofakeit/v7 v7.7.1
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225
|
||||
github.com/coder/aibridge v0.1.3
|
||||
github.com/coder/aibridge v0.1.4-0.20251112094427-5899d515872f
|
||||
github.com/coder/aisdk-go v0.0.9
|
||||
github.com/coder/boundary v1.0.1-0.20250925154134-55a44f2a7945
|
||||
github.com/coder/preview v1.0.4
|
||||
|
||||
8
go.sum
8
go.sum
@@ -911,16 +911,16 @@ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 h1:tRIViZ5JRmzdOEo5wUWngaGEFBG8OaE1o2GIHN5ujJ8=
|
||||
github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225/go.mod h1:rNLVpYgEVeu1Zk29K64z6Od8RBP9DwqCu9OfCzh8MR4=
|
||||
github.com/coder/aibridge v0.1.3 h1:7A9RQaHQUjtse47ShF3kBj2hMmT1R7BEFgiyByr8Vvc=
|
||||
github.com/coder/aibridge v0.1.3/go.mod h1:GWc0Owtlzz5iMHosDm6FhbO+SoG5W+VeOKyP9p9g9ZM=
|
||||
github.com/coder/aibridge v0.1.4-0.20251112094427-5899d515872f h1:uaBJAuI2t7Al+Q6G8JHZL2TmGwImLCGmkh+gyBK9Dvs=
|
||||
github.com/coder/aibridge v0.1.4-0.20251112094427-5899d515872f/go.mod h1:GWc0Owtlzz5iMHosDm6FhbO+SoG5W+VeOKyP9p9g9ZM=
|
||||
github.com/coder/aisdk-go v0.0.9 h1:Vzo/k2qwVGLTR10ESDeP2Ecek1SdPfZlEjtTfMveiVo=
|
||||
github.com/coder/aisdk-go v0.0.9/go.mod h1:KF6/Vkono0FJJOtWtveh5j7yfNrSctVTpwgweYWSp5M=
|
||||
github.com/coder/boundary v1.0.1-0.20250925154134-55a44f2a7945 h1:hDUf02kTX8EGR3+5B+v5KdYvORs4YNfDPci0zCs+pC0=
|
||||
github.com/coder/boundary v1.0.1-0.20250925154134-55a44f2a7945/go.mod h1:d1AMFw81rUgrGHuZzWdPNhkY0G8w7pvLNLYF0e3ceC4=
|
||||
github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41 h1:SBN/DA63+ZHwuWwPHPYoCZ/KLAjHv5g4h2MS4f2/MTI=
|
||||
github.com/coder/bubbletea v1.2.2-0.20241212190825-007a1cdb2c41/go.mod h1:I9ULxr64UaOSUv7hcb3nX4kowodJCVS7vt7VVJk/kW4=
|
||||
github.com/coder/clistat v1.0.0 h1:MjiS7qQ1IobuSSgDnxcCSyBPESs44hExnh2TEqMcGnA=
|
||||
github.com/coder/clistat v1.0.0/go.mod h1:F+gLef+F9chVrleq808RBxdaoq52R4VLopuLdAsh8Y4=
|
||||
github.com/coder/clistat v1.1.1 h1:T45dlwr7fSmjLPGLk7QRKgynnDeMOPoraHSGtLIHY3s=
|
||||
github.com/coder/clistat v1.1.1/go.mod h1:F+gLef+F9chVrleq808RBxdaoq52R4VLopuLdAsh8Y4=
|
||||
github.com/coder/flog v1.1.0 h1:kbAes1ai8fIS5OeV+QAnKBQE22ty1jRF/mcAwHpLBa4=
|
||||
github.com/coder/flog v1.1.0/go.mod h1:UQlQvrkJBvnRGo69Le8E24Tcl5SJleAAR7gYEHzAmdQ=
|
||||
github.com/coder/glog v1.0.1-0.20220322161911-7365fe7f2cd1/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4=
|
||||
|
||||
43
helm/coder/tests/testdata/namespace_rbac.golden
vendored
43
helm/coder/tests/testdata/namespace_rbac.golden
vendored
@@ -117,34 +117,6 @@ rules:
|
||||
# Source: coder/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: coder-workspace-perms
|
||||
namespace: test-namespace2
|
||||
rules:
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
---
|
||||
# Source: coder/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: coder-workspace-perms
|
||||
namespace: test-namespace3
|
||||
@@ -262,21 +234,6 @@ roleRef:
|
||||
# Source: coder/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: "coder"
|
||||
namespace: test-namespace2
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: "coder"
|
||||
namespace: default
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: coder-workspace-perms
|
||||
---
|
||||
# Source: coder/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: "coder"
|
||||
namespace: test-namespace3
|
||||
|
||||
@@ -117,34 +117,6 @@ rules:
|
||||
# Source: coder/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: coder-workspace-perms
|
||||
namespace: test-namespace2
|
||||
rules:
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- deployments
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- deletecollection
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
---
|
||||
# Source: coder/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: coder-workspace-perms
|
||||
namespace: test-namespace3
|
||||
@@ -262,21 +234,6 @@ roleRef:
|
||||
# Source: coder/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: "coder"
|
||||
namespace: test-namespace2
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: "coder"
|
||||
namespace: coder
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: coder-workspace-perms
|
||||
---
|
||||
# Source: coder/templates/rbac.yaml
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: "coder"
|
||||
namespace: test-namespace3
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{{- define "libcoder.rbac.forNamespace" -}}
|
||||
{{- $nsPerms := ternary .workspacePerms .Top.Values.coder.serviceAccount.workspacePerms (hasKey . "workspacePerms") -}}
|
||||
{{- $nsDeploy := ternary .enableDeployments .Top.Values.coder.serviceAccount.enableDeployments (hasKey . "enableDeployments") -}}
|
||||
{{- $nsExtra := ternary .extraRules .Top.Values.coder.serviceAccount.extraRules (hasKey . "extraRules") -}}
|
||||
{{- $nsDeployRaw := ternary .enableDeployments .Top.Values.coder.serviceAccount.enableDeployments (hasKey . "enableDeployments") -}}
|
||||
{{- $nsExtraRaw := ternary .extraRules .Top.Values.coder.serviceAccount.extraRules (hasKey . "extraRules") -}}
|
||||
{{- $nsDeploy := and $nsPerms $nsDeployRaw -}}
|
||||
{{- $nsExtra := ternary $nsExtraRaw (list) $nsPerms -}}
|
||||
|
||||
{{- if or $nsPerms (or $nsDeploy $nsExtra) }}
|
||||
---
|
||||
|
||||
@@ -54,10 +54,19 @@ func newPty(opt ...Option) (*ptyWindows, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
consoleSize := uintptr(80) + (uintptr(80) << 16)
|
||||
// Default dimensions
|
||||
width, height := 80, 80
|
||||
if opts.sshReq != nil {
|
||||
consoleSize = uintptr(opts.sshReq.Window.Width) + (uintptr(opts.sshReq.Window.Height) << 16)
|
||||
if w := opts.sshReq.Window.Width; w > 0 && w <= 65535 {
|
||||
width = w
|
||||
}
|
||||
if h := opts.sshReq.Window.Height; h > 0 && h <= 65535 {
|
||||
height = h
|
||||
}
|
||||
}
|
||||
|
||||
consoleSize := uintptr(width) + (uintptr(height) << 16)
|
||||
|
||||
ret, _, err := procCreatePseudoConsole.Call(
|
||||
consoleSize,
|
||||
uintptr(pty.inputRead.Fd()),
|
||||
|
||||
@@ -51,10 +51,7 @@ fi
|
||||
|
||||
image="${CODER_IMAGE_BASE:-ghcr.io/coder/coder}"
|
||||
|
||||
# use CODER_IMAGE_TAG_PREFIX if set as a prefix for the tag
|
||||
tag_prefix="${CODER_IMAGE_TAG_PREFIX:-}"
|
||||
|
||||
tag="${tag_prefix:+$tag_prefix-}v$version"
|
||||
tag="v$version"
|
||||
|
||||
if [[ "$version" == "latest" ]]; then
|
||||
tag="latest"
|
||||
|
||||
68
scripts/should_deploy.sh
Executable file
68
scripts/should_deploy.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# This script determines if a commit in either the main branch or a
|
||||
# `release/x.y` branch should be deployed to dogfood.
|
||||
#
|
||||
# To avoid masking unrelated failures, this script will return 0 in either case,
|
||||
# and will print `DEPLOY` or `NOOP` to stdout.
|
||||
|
||||
set -euo pipefail
|
||||
# shellcheck source=scripts/lib.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/lib.sh"
|
||||
cdroot
|
||||
|
||||
deploy_branch=main
|
||||
|
||||
# Determine the current branch name and check that it is one of the supported
|
||||
# branch names.
|
||||
branch_name=$(git branch --show-current)
|
||||
if [[ "$branch_name" != "main" && ! "$branch_name" =~ ^release/[0-9]+\.[0-9]+$ ]]; then
|
||||
error "Current branch '$branch_name' is not a supported branch name for dogfood, must be 'main' or 'release/x.y'"
|
||||
fi
|
||||
log "Current branch '$branch_name'"
|
||||
|
||||
# Determine the remote name
|
||||
remote=$(git remote -v | grep coder/coder | awk '{print $1}' | head -n1)
|
||||
if [[ -z "${remote}" ]]; then
|
||||
error "Could not find remote for coder/coder"
|
||||
fi
|
||||
log "Using remote '$remote'"
|
||||
|
||||
# Step 1: List all release branches and sort them by major/minor so we can find
|
||||
# the latest release branch.
|
||||
release_branches=$(
|
||||
git branch -r --format='%(refname:short)' |
|
||||
grep -E "${remote}/release/[0-9]+\.[0-9]+$" |
|
||||
sed "s|${remote}/||" |
|
||||
sort -V
|
||||
)
|
||||
|
||||
# As a sanity check, release/2.26 should exist.
|
||||
if ! echo "$release_branches" | grep "release/2.26" >/dev/null; then
|
||||
error "Could not find existing release branches. Did you run 'git fetch -ap ${remote}'?"
|
||||
fi
|
||||
|
||||
latest_release_branch=$(echo "$release_branches" | tail -n 1)
|
||||
latest_release_branch_version=${latest_release_branch#release/}
|
||||
log "Latest release branch: $latest_release_branch"
|
||||
log "Latest release branch version: $latest_release_branch_version"
|
||||
|
||||
# Step 2: check if a matching tag `v<x.y>.0` exists. If it does not, we will
|
||||
# use the release branch as the deploy branch.
|
||||
if ! git rev-parse "refs/tags/v${latest_release_branch_version}.0" >/dev/null 2>&1; then
|
||||
log "Tag 'v${latest_release_branch_version}.0' does not exist, using release branch as deploy branch"
|
||||
deploy_branch=$latest_release_branch
|
||||
else
|
||||
log "Matching tag 'v${latest_release_branch_version}.0' exists, using main as deploy branch"
|
||||
fi
|
||||
log "Deploy branch: $deploy_branch"
|
||||
|
||||
# Finally, check if the current branch is the deploy branch.
|
||||
log
|
||||
if [[ "$branch_name" != "$deploy_branch" ]]; then
|
||||
log "VERDICT: DO NOT DEPLOY"
|
||||
echo "NOOP" # stdout
|
||||
else
|
||||
log "VERDICT: DEPLOY"
|
||||
echo "DEPLOY" # stdout
|
||||
fi
|
||||
@@ -55,7 +55,7 @@
|
||||
"@radix-ui/react-avatar": "1.1.2",
|
||||
"@radix-ui/react-checkbox": "1.1.4",
|
||||
"@radix-ui/react-collapsible": "1.1.2",
|
||||
"@radix-ui/react-dialog": "1.1.15",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||
"@radix-ui/react-label": "2.1.0",
|
||||
"@radix-ui/react-popover": "1.1.5",
|
||||
@@ -94,11 +94,11 @@
|
||||
"lucide-react": "0.474.0",
|
||||
"monaco-editor": "0.52.2",
|
||||
"pretty-bytes": "6.1.1",
|
||||
"react": "19.1.1",
|
||||
"react": "19.2.1",
|
||||
"react-color": "2.19.3",
|
||||
"react-confetti": "6.2.2",
|
||||
"react-date-range": "1.4.0",
|
||||
"react-dom": "19.1.1",
|
||||
"react-dom": "19.2.1",
|
||||
"react-markdown": "9.1.0",
|
||||
"react-query": "npm:@tanstack/react-query@5.77.0",
|
||||
"react-resizable-panels": "3.0.3",
|
||||
@@ -126,7 +126,7 @@
|
||||
"@biomejs/biome": "2.2.0",
|
||||
"@chromatic-com/storybook": "4.1.0",
|
||||
"@octokit/types": "12.3.0",
|
||||
"@playwright/test": "1.55.1",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@storybook/addon-docs": "9.1.2",
|
||||
"@storybook/addon-links": "9.1.2",
|
||||
"@storybook/addon-themes": "9.1.2",
|
||||
@@ -169,7 +169,7 @@
|
||||
"jest-websocket-mock": "2.5.0",
|
||||
"jest_workaround": "0.1.14",
|
||||
"knip": "5.64.1",
|
||||
"msw": "2.11.3",
|
||||
"msw": "2.4.8",
|
||||
"postcss": "8.5.1",
|
||||
"protobufjs": "7.4.0",
|
||||
"rollup-plugin-visualizer": "5.14.0",
|
||||
|
||||
1698
site/pnpm-lock.yaml
generated
1698
site/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -119,7 +119,7 @@ export const CreateWorkspacePageViewExperimental: FC<
|
||||
// Only touched fields are sent to the websocket
|
||||
// Autofilled parameters are marked as touched since they have been modified
|
||||
const initialTouched = Object.fromEntries(
|
||||
parameters.filter((p) => autofillByName[p.name]).map((p) => [p, true]),
|
||||
parameters.filter((p) => autofillByName[p.name]).map((p) => [p.name, true]),
|
||||
);
|
||||
|
||||
// The form parameters values hold the working state of the parameters that will be submitted when creating a workspace
|
||||
|
||||
@@ -48,18 +48,6 @@ export const WorkspaceParametersPageViewExperimental: FC<
|
||||
onCancel,
|
||||
templateVersionId,
|
||||
}) => {
|
||||
const autofillByName = Object.fromEntries(
|
||||
autofillParameters.map((param) => [param.name, param]),
|
||||
);
|
||||
const initialTouched = parameters.reduce(
|
||||
(touched, parameter) => {
|
||||
if (autofillByName[parameter.name] !== undefined) {
|
||||
touched[parameter.name] = true;
|
||||
}
|
||||
return touched;
|
||||
},
|
||||
{} as Record<string, boolean>,
|
||||
);
|
||||
const form = useFormik({
|
||||
onSubmit,
|
||||
initialValues: {
|
||||
@@ -68,7 +56,6 @@ export const WorkspaceParametersPageViewExperimental: FC<
|
||||
autofillParameters,
|
||||
),
|
||||
},
|
||||
initialTouched,
|
||||
validationSchema: useValidationSchemaForDynamicParameters(parameters),
|
||||
enableReinitialize: false,
|
||||
validateOnChange: true,
|
||||
@@ -89,28 +76,23 @@ export const WorkspaceParametersPageViewExperimental: FC<
|
||||
name: parameter.name,
|
||||
value,
|
||||
});
|
||||
form.setFieldTouched(parameter.name, true);
|
||||
sendDynamicParamsRequest(parameter, value);
|
||||
};
|
||||
|
||||
// Send the changed parameter and all touched parameters to the websocket
|
||||
const sendDynamicParamsRequest = (
|
||||
parameter: PreviewParameter,
|
||||
value: string,
|
||||
) => {
|
||||
const formInputs: Record<string, string> = {};
|
||||
formInputs[parameter.name] = value;
|
||||
const parameters = form.values.rich_parameter_values ?? [];
|
||||
|
||||
for (const [fieldName, isTouched] of Object.entries(form.touched)) {
|
||||
if (isTouched && fieldName !== parameter.name) {
|
||||
const param = parameters.find((p) => p.name === fieldName);
|
||||
if (param?.value) {
|
||||
formInputs[fieldName] = param.value;
|
||||
}
|
||||
for (const param of parameters) {
|
||||
if (param?.name && param?.value) {
|
||||
formInputs[param.name] = param.value;
|
||||
}
|
||||
}
|
||||
|
||||
formInputs[parameter.name] = value;
|
||||
|
||||
sendMessage(formInputs);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user