From 6dc252f81bab299dc02cfd6bb44a7925012e02af Mon Sep 17 00:00:00 2001 From: Oliver Dragland Date: Fri, 18 Sep 2026 11:33:08 +0200 Subject: [PATCH 1/2] feature(ci): sett opp runners og deploy til VM --- .github/actionlint.yaml | 4 ++ .github/workflows/ci.yml | 120 ++++++++++++++++++++++++++++++++ deploy/apache-project1.conf | 15 ++++ scripts/deploy-project1.sh | 96 +++++++++++++++++++++++++ scripts/test-deploy-project1.sh | 99 ++++++++++++++++++++++++++ 5 files changed, 334 insertions(+) create mode 100644 .github/actionlint.yaml create mode 100644 .github/workflows/ci.yml create mode 100644 deploy/apache-project1.conf create mode 100644 scripts/deploy-project1.sh create mode 100644 scripts/test-deploy-project1.sh diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 0000000..9e78517 --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,4 @@ +self-hosted-runner: + labels: + - t31-ci + - t31-deploy diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..702639e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,120 @@ +name: CI + +on: + push: + branches: [dev, main] + pull_request: + branches: [dev, main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + # la main bli ferdig, også hvis vi må rulle tilbake + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + +defaults: + run: + shell: bash + +jobs: + quality: + name: Quality checks + runs-on: [self-hosted, linux, x64, t31-ci] + timeout-minutes: 30 + env: + CI: 'true' + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .node-version + + - name: Use the pinned npm version + run: npm install --global npm@11.11.1 + + - name: Install dependencies + run: npm ci + + - name: Check code, contracts, migration metadata, tests and builds + run: npm run check + + - name: Test release switching and rollback + run: bash scripts/test-deploy-project1.sh + + - name: Install test browsers + run: npm run test:e2e:install + + - name: Run browser checks + run: npm run test:e2e -- --workers=2 + + - name: Preserve browser failure evidence + if: failure() + # git.ntnu.no støtter ikke artifact v4+, derfor denne versjonen + uses: actions/upload-artifact@v3.2.2-node20 + with: + name: browser-results-${{ github.run_id }}-${{ github.run_attempt }} + path: test-results/ + retention-days: 7 + + - name: Save the tested production build + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + uses: actions/upload-artifact@v3.2.2-node20 + with: + name: project1-${{ github.sha }}-${{ github.run_attempt }} + path: web/dist/ + if-no-files-found: error + retention-days: 7 + + deploy: + name: Deploy Project 1 + needs: quality + if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' + runs-on: [self-hosted, linux, x64, t31-deploy] + timeout-minutes: 10 + environment: + name: production + url: http://it2810-31.idi.ntnu.no/project1/ + concurrency: + group: project1-production + cancel-in-progress: false + steps: + - name: Check that this commit is still the tip of main + id: latest + uses: actions/github-script@v7 + with: + script: | + const { data } = await github.rest.repos.getCommit({ + ...context.repo, + ref: 'main', + }); + core.setOutput('current', data.sha === context.sha); + if (data.sha !== context.sha) { + core.notice('A newer commit is on main; skip this outdated deployment.'); + } + + - name: Check out the deployment script from the tested commit + if: steps.latest.outputs.current == 'true' + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Download the tested production build + if: steps.latest.outputs.current == 'true' + uses: actions/download-artifact@v3-node20 + with: + name: project1-${{ github.sha }}-${{ github.run_attempt }} + path: web/dist/ + + - name: Publish to Apache and verify the release + if: steps.latest.outputs.current == 'true' + env: + RELEASE_ID: ${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }} + run: bash scripts/deploy-project1.sh web/dist "$RELEASE_ID" diff --git a/deploy/apache-project1.conf b/deploy/apache-project1.conf new file mode 100644 index 0000000..53e2bf4 --- /dev/null +++ b/deploy/apache-project1.conf @@ -0,0 +1,15 @@ +# slå på med a2enconf t31-project1, sjekk configen og last apache på nytt +RedirectMatch 302 ^/project1$ /project1/ +Alias /project1/ /srv/t31-project1/current/ + + + Options +FollowSymLinks -Indexes -ExecCGI -Includes + AllowOverride None + Require all granted + DirectoryIndex index.html + # prosjekt 1 er bare statiske filer, ikke kjør noe backend her + SetHandler default-handler + + Header set Cache-Control "no-cache" + + diff --git a/scripts/deploy-project1.sh b/scripts/deploy-project1.sh new file mode 100644 index 0000000..41111cc --- /dev/null +++ b/scripts/deploy-project1.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# kjør som t31-deploy, uten sudo. apache leser fra current +# ryddet opp med hjelp fra codex +set -Eeuo pipefail +umask 022 + +fail() { + printf '%s\n' "$*" >&2 + exit 1 +} + +check_input() { + [[ $release_id =~ ^[a-f0-9]{40}-[0-9]+-[0-9]+$ ]] || fail 'ugyldig release-id' + [[ -d $deploy_root/releases && -w $deploy_root ]] || fail 'deploy-mappa må settes opp først' + [[ -s $source_dir/index.html && -d $source_dir/assets ]] || fail 'bygget mangler index.html eller assets' + [[ -z $(find "$source_dir" -mindepth 1 ! -type f ! -type d -print -quit) ]] || fail 'bygget kan bare inneholde vanlige filer og mapper' + [[ ! -e $deploy_root/current || -L $deploy_root/current ]] || fail 'current må være en symlink' + [[ $health_url == http://* || $health_url == https://* ]] || fail 'sjekk-adressen må bruke http eller https' +} + +# bytt hele siden på en gang, så ingen får et halvferdig bygg +set_current() { + rm -f -- "$work_dir/current" + ln -s -- "$1" "$work_dir/current" + mv -Tf -- "$work_dir/current" "$deploy_root/current" +} + +cleanup() { + local status=$? + trap - EXIT HUP INT TERM + + # noe feilet etter byttet? legg tilbake forrige versjon + if [[ $status != 0 && $switched == true ]]; then + if [[ -n $previous ]]; then + set_current "$previous" + printf 'deploy feilet, gikk tilbake til %s\n' "$previous" >&2 + else + rm -- "$deploy_root/current" + printf 'første deploy feilet, fjernet current\n' >&2 + fi + fi + rm -rf -- "$work_dir" + exit "$status" +} + +prepare_release() { + mkdir "$work_dir/build" + cp -R -- "$source_dir/." "$work_dir/build/" + printf '%s\n' "$release_id" > "$work_dir/build/release.txt" + find "$work_dir/build" -type d -exec chmod 755 {} + + find "$work_dir/build" -type f -exec chmod 644 {} + + mv -T -- "$work_dir/build" "$release_dir" +} + +# et 200-svar er ikke nok, apache må sende akkurat den fila vi bygde +check_file() { + local file=$1 url_path=${2-$1} + curl --fail --silent --show-error --noproxy '*' --connect-timeout 5 --max-time 15 \ + "$health_url/$url_path" -o "$work_dir/response" || fail "fikk ikke hentet $file fra apache" + cmp -s -- "$release_dir/$file" "$work_dir/response" || fail "apache sender feil innhold for $file" +} + +check_release() { + check_file release.txt + check_file index.html '' # sjekk selve /project1/, ikke bare index.html + find "$release_dir/assets" -type f -printf '%P\0' > "$work_dir/assets" + while IFS= read -r -d '' asset; do + [[ $asset =~ ^[A-Za-z0-9_./-]+$ ]] || fail 'uventet filnavn i assets' + check_file "assets/$asset" + done < "$work_dir/assets" +} + +[[ $# == 2 ]] || fail 'bruk: deploy-project1.sh BYGGMAPPE RELEASE_ID' +source_dir=$(realpath -- "$1") +release_id=$2 +deploy_root=${T31_DEPLOY_ROOT:-/srv/t31-project1} +health_url=${T31_DEPLOY_URL:-http://127.0.0.1/project1/} +health_url=${health_url%/} +check_input + +# bare en deploy av gangen, også om noen kjører scriptet manuelt +exec 9>"$deploy_root/deploy.lock" +flock -w 60 9 || fail 'en annen deploy holder på' +release_dir=$deploy_root/releases/$release_id +[[ ! -e $release_dir ]] || fail 'denne releasen finnes fra før, start en ny kjøring' +previous=$(readlink "$deploy_root/current" || true) +work_dir=$(mktemp -d "$deploy_root/.deploy-XXXXXXXX") +switched=false +trap cleanup EXIT +trap 'exit 1' HUP INT TERM + +prepare_release +switched=true # rollback må være klar før vi bytter +set_current "$release_dir" +check_release +printf 'publisert %s på %s/\n' "$release_id" "$health_url" diff --git a/scripts/test-deploy-project1.sh b/scripts/test-deploy-project1.sh new file mode 100644 index 0000000..4e3edd2 --- /dev/null +++ b/scripts/test-deploy-project1.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# sjekker deploy og rollback med en liten lokal webserver +set -Eeuo pipefail +repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +scratch=$(mktemp -d) +server_pid='' +cleanup() { + if [[ -n $server_pid ]]; then + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true + fi + rm -rf -- "$scratch" +} +trap cleanup EXIT +trap 'exit 1' HUP INT TERM +mkdir -p "$scratch/site/releases" "$scratch/build/assets" "$scratch/http" +ln -s "$scratch/site/current" "$scratch/http/project1" +printf '\n' > "$scratch/build/index.html" +printf 'console.log("release one");\n' > "$scratch/build/assets/app.js" + +python3 - "$scratch" <<'PY' & +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +import sys + +root = Path(sys.argv[1]) + +class Handler(SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=str(root / 'http'), **kwargs) + + def do_GET(self): + if ((root / 'bad-index').exists() and self.path in ['/project1/', '/project1/index.html']) or ( + (root / 'bad-asset').exists() and '/assets/' in self.path + ): + self.send_response(200) + self.end_headers() + self.wfile.write(b'wrong release') + else: + super().do_GET() + + def log_message(self, *args): + pass + +server = ThreadingHTTPServer(('127.0.0.1', 0), Handler) +(root / 'port').write_text(str(server.server_port)) +server.serve_forever() +PY +server_pid=$! +for ((attempt=0; attempt<100; attempt++)); do + [[ -s $scratch/port ]] && break + sleep 0.05 +done +[[ -s $scratch/port ]] || { printf 'HTTP server did not start.\n' >&2; exit 1; } + +export T31_DEPLOY_ROOT=$scratch/site +T31_DEPLOY_URL=http://127.0.0.1:$(cat "$scratch/port")/project1/ +export T31_DEPLOY_URL +first=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1-1 +second=bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb-2-1 +failed=cccccccccccccccccccccccccccccccccccccccc-3-1 +deploy() { bash "$repo_root/scripts/deploy-project1.sh" "$scratch/build" "$1"; } +expect_failure() { + if deploy "$1" > "$scratch/failure.log" 2>&1; then + printf 'Expected deployment %s to fail.\n' "$1" >&2 + exit 1 + fi +} + +touch "$scratch/bad-index" +expect_failure "$failed" +[[ ! -e $scratch/site/current && ! -L $scratch/site/current ]] +rm "$scratch/bad-index" +deploy "$first" +[[ $(readlink "$scratch/site/current") == "$scratch/site/releases/$first" ]] +cmp "$scratch/build/assets/app.js" "$scratch/site/current/assets/app.js" +[[ $(stat -c '%a' "$scratch/site/current/index.html") == 644 ]] + +printf 'console.log("release two");\n' > "$scratch/build/assets/app.js" +deploy "$second" +[[ -f $scratch/site/releases/$first/index.html ]] +[[ $(readlink "$scratch/site/current") == "$scratch/site/releases/$second" ]] +expect_failure "$second" +expect_failure '../../escape' +ln -s /etc/passwd "$scratch/build/assets/link" +expect_failure "${failed%1}2" +rm "$scratch/build/assets/link" + +touch "$scratch/bad-index" +expect_failure "${failed%1}3" +[[ $(readlink "$scratch/site/current") == "$scratch/site/releases/$second" ]] +rm "$scratch/bad-index" +touch "$scratch/bad-asset" +expect_failure "${failed%1}4" +[[ $(readlink "$scratch/site/current") == "$scratch/site/releases/$second" ]] +rm "$scratch/bad-asset" +curl --fail --silent --noproxy '*' "$T31_DEPLOY_URL/release.txt" | cmp - "$scratch/site/current/release.txt" +[[ -z $(find "$scratch/site" -maxdepth 1 -name '.deploy-*' -print -quit) ]] +printf 'Deployment checks passed: first release, update, validation, index/asset rollback and cleanup.\n' From cfc6a80ecaea894e430425f28081c8405960f6ab Mon Sep 17 00:00:00 2001 From: Oliver Dragland Date: Fri, 18 Sep 2026 11:36:27 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix(ci):=20bruk=20artifact-versjonen=20p?= =?UTF-8?q?=C3=A5=20NTNU=20GitHub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 702639e..7e8bf78 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,8 +57,8 @@ jobs: - name: Preserve browser failure evidence if: failure() - # git.ntnu.no støtter ikke artifact v4+, derfor denne versjonen - uses: actions/upload-artifact@v3.2.2-node20 + # denne node20-versjonen finnes på git.ntnu.no og støtter enterprise + uses: actions/upload-artifact@v3.2.1-node20 with: name: browser-results-${{ github.run_id }}-${{ github.run_attempt }} path: test-results/ @@ -66,7 +66,7 @@ jobs: - name: Save the tested production build if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' - uses: actions/upload-artifact@v3.2.2-node20 + uses: actions/upload-artifact@v3.2.1-node20 with: name: project1-${{ github.sha }}-${{ github.run_attempt }} path: web/dist/