Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
142 changes: 142 additions & 0 deletions .github/scripts/fail_on_changed_line_lint.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2026 The Outline Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {existsSync, readFileSync} from 'node:fs';
import path from 'node:path';
import {spawnSync} from 'node:child_process';

function getRequiredEnv(name) {
const value = process.env[name];
if (!value) {
throw new Error(`${name} is required`);
}
return value;
}

function parseFileList(value) {
return value.trim().split(/\s+/).filter(Boolean);
}

function normalizePath(filePath) {
return path.isAbsolute(filePath)
? path.relative(process.cwd(), filePath)
: filePath;
}

function getChangedLineSet(baseCommit, headCommit, file) {
const result = spawnSync(
'git',
['diff', '--unified=0', '--no-color', baseCommit, headCommit, '--', file],
{encoding: 'utf8'}
);
if (result.status !== 0) {
throw new Error(`git diff failed for ${file}: ${result.stderr}`);
}

const lines = new Set();
const hunkRegExp = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/;
for (const line of result.stdout.split('\n')) {
const match = line.match(hunkRegExp);
if (!match) {
continue;
}
const start = Number.parseInt(match[1], 10);
const count = Number.parseInt(match[2] || '1', 10);
for (let i = 0; i < count; i++) {
lines.add(start + i);
}
}
return lines;
}

function getLine(message, key) {
const line = message[key];
return Number.isInteger(line) ? line : null;
}

function main() {
const baseCommit = getRequiredEnv('BASE_COMMIT');
const headCommit = getRequiredEnv('HEAD_COMMIT');
const lintResultsPath = process.env.LINT_RESULTS_PATH || 'lint-results.json';
const files = [
...parseFileList(process.env.MODIFIED_FILES || ''),
...parseFileList(process.env.ADDED_FILES || ''),
];

if (!files.length) {
return;
}
if (!existsSync(lintResultsPath)) {
throw new Error(`Missing lint results file: ${lintResultsPath}`);
}

const changedLinesByFile = new Map(
files.map(file => [file, getChangedLineSet(baseCommit, headCommit, file)])
);
const lintResults = JSON.parse(readFileSync(lintResultsPath, 'utf8'));
const blockingIssues = [];

for (const fileResult of lintResults) {
const file = normalizePath(fileResult.filePath);
const changedLines = changedLinesByFile.get(file);
if (!changedLines) {
continue;
}

for (const message of fileResult.messages || []) {
if (!(message.severity === 2 || message.fatal)) {
continue;
}

const line = getLine(message, 'line');
const endLine = getLine(message, 'endLine') || line;
if (!line) {
// File-level parse/config errors.
blockingIssues.push({file, line: 1, message});
continue;
}

let intersectsChangedLines = false;
for (let current = line; current <= endLine; current++) {
if (changedLines.has(current)) {
intersectsChangedLines = true;
break;
}
}
if (intersectsChangedLines) {
blockingIssues.push({file, line, message});
}
}
}

if (!blockingIssues.length) {
console.log('No lint errors found on changed lines.');
return;
}

for (const issue of blockingIssues) {
const rule = issue.message.ruleId || 'lint';
const text = issue.message.message.replace(/\r?\n/g, ' ');
console.log(
`::error file=${issue.file},line=${issue.line},title=${rule}::${text}`
);
}

console.error(
`Found ${blockingIssues.length} lint error(s) on changed lines.`
);
process.exit(1);
}

main();
34 changes: 32 additions & 2 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ jobs:
steps:
- name: Checkout
uses: actions/[email protected]
with:
fetch-depth: 0

- name: Install Node
uses: actions/setup-node@v3
Expand All @@ -55,7 +57,35 @@ jobs:
**/*.cjs
**/*.mjs

- name: Lint
- name: Set base and head commits
id: commits
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
base_commit="${{ github.event.pull_request.base.sha }}"
else
base_commit="${{ github.event.before }}"
# Handle edge cases where "before" is empty or all-zero (e.g. first push).
if [[ -z "$base_commit" || "$base_commit" =~ ^0+$ ]]; then
base_commit="$(git rev-list --max-parents=0 HEAD)"
fi
fi
echo "base_commit=$base_commit" >> "$GITHUB_OUTPUT"
echo "head_commit=${{ github.sha }}" >> "$GITHUB_OUTPUT"

- name: Lint changed files
if: ${{ steps.changed-files.outputs.modified_files_count > 0 || steps.changed-files.outputs.added_file_count > 0 }}
run: |
npx eslint -f json -o lint-results.json \
${{ steps.changed-files.outputs.modified_files }} \
${{ steps.changed-files.outputs.added_files }} || true

- name: Fail on issues in changed lines
if: ${{ steps.changed-files.outputs.modified_files_count > 0 || steps.changed-files.outputs.added_file_count > 0 }}
run: npx gts lint ${{ steps.changed-files.outputs.modified_files }} ${{ steps.changed-files.outputs.added_files }}
env:
BASE_COMMIT: ${{ steps.commits.outputs.base_commit }}
HEAD_COMMIT: ${{ steps.commits.outputs.head_commit }}
MODIFIED_FILES: ${{ steps.changed-files.outputs.modified_files }}
ADDED_FILES: ${{ steps.changed-files.outputs.added_files }}
LINT_RESULTS_PATH: lint-results.json
run: node .github/scripts/fail_on_changed_line_lint.mjs

36 changes: 36 additions & 0 deletions server_manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,42 @@ To run the Outline Manager as a web app on the browser and listen for changes:
npm run action server_manager/www/start
```

## Updating Cloud Locations

When new cloud regions/zones appear (especially for GCP), update all of the
following so the location picker shows proper city and country data.

1. Add geolocation constants in `server_manager/model/location.ts`.
2. Map cloud region IDs to `GeoLocation` in `server_manager/model/gcp.ts`
(`Zone.LOCATION_MAP`).
3. Add localized city message keys:
- Source messages: `server_manager/messages/master_messages.json` using
`geo_*` keys.
- Runtime English messages: `server_manager/messages/en.json` and
`server_manager/messages/en-GB.json` using `geo-*` keys.
4. Keep all `geo_*` / `geo-*` message keys alphabetically sorted.
5. Update tests:
- `server_manager/model/gcp.spec.ts` region coverage list and assertions.
- `server_manager/www/location_formatting.spec.ts` if formatting/sorting
behavior changes.
6. Verify:

```bash
npx tsc -p server_manager --outDir output/build/js/server_manager --module commonjs
npx jasmine output/build/js/server_manager/model/gcp.spec.js output/build/js/server_manager/www/location_formatting.spec.js
npm run action server_manager/www/test
```

Notes:
- Missing entries in `Zone.LOCATION_MAP` cause unknown location cards (`?` and
raw zone IDs) in the picker.
- Region discovery can be checked programmatically with:

```bash
gcloud compute regions list --format="value(name)"
gcloud compute zones list --format="value(name)"
```

## Debug an existing binary

You can run an existing binary in debug mode by setting `OUTLINE_DEBUG=true`.
Expand Down
11 changes: 5 additions & 6 deletions server_manager/install_scripts/gcp_install_server.sh
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function log_for_sentry() {
}

function post_sentry_report() {
if [[ -n "${SENTRY_API_URL}" ]]; then
if [[ -n "${SENTRY_API_URL:-}" ]]; then
# Get JSON formatted string. This command replaces newlines with literal '\n'
# but otherwise assumes that there are no other characters to escape for JSON.
# If we need better escaping, we can install the jq command line tool.
Expand All @@ -69,7 +69,6 @@ function cloud::set_guest_attribute() {
cloud::set_guest_attribute "install-started" "true"

# Enable BBR.
# Recent DigitalOcean one-click images are based on Ubuntu 18 and have kernel 4.15+.
log_for_sentry "Enabling BBR"
cat >> /etc/sysctl.conf << EOF

Expand Down Expand Up @@ -101,15 +100,15 @@ log_for_sentry "Downloading Docker"
# Following instructions from https://docs.docker.com/engine/install/ubuntu/#install-from-a-package

declare -ar PACKAGES=(
'containerd.io_1.4.9-1_amd64.deb'
'docker-ce_20.10.8~3-0~ubuntu-focal_amd64.deb'
'docker-ce-cli_20.10.8~3-0~ubuntu-focal_amd64.deb'
'containerd.io_2.2.1-1~ubuntu.24.04~noble_amd64.deb'
'docker-ce_29.2.1-1~ubuntu.24.04~noble_amd64.deb'
'docker-ce-cli_29.2.1-1~ubuntu.24.04~noble_amd64.deb'
)

declare packages_csv
packages_csv="$(printf ',%s' "${PACKAGES[@]}")"
packages_csv="${packages_csv:1}"
curl --remote-name-all --fail "https://download.docker.com/linux/ubuntu/dists/focal/pool/stable/amd64/{${packages_csv}}"
curl --remote-name-all --fail --location --retry 5 --retry-delay 5 --connect-timeout 10 --max-time 120 "https://download.docker.com/linux/ubuntu/dists/noble/pool/stable/amd64/{${packages_csv}}"
log_for_sentry "Installing Docker"
dpkg --install "${PACKAGES[@]}"
rm "${PACKAGES[@]}"
Expand Down
15 changes: 15 additions & 0 deletions server_manager/messages/en-GB.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,35 +123,50 @@
"gcp-type-outline-server": "Type 'outline-server' in the 'Name' field.",
"geo-amsterdam": "Amsterdam",
"geo-bangalore": "Bangalore",
"geo-berlin": "Berlin",
"geo-changhua-county": "Changhua County",
"geo-columbus": "Columbus",
"geo-dallas": "Dallas",
"geo-dammam": "Dammam",
"geo-delhi": "Delhi",
"geo-doha": "Doha",
"geo-eemshaven": "Eemshaven",
"geo-frankfurt": "Frankfurt",
"geo-hamina": "Hamina",
"geo-hk": "Hong Kong",
"geo-iowa": "Iowa",
"geo-jakarta": "Jakarta",
"geo-johannesburg": "Johannesburg",
"geo-jurong-west": "Jurong West",
"geo-kuala-lumpur": "Kuala Lumpur",
"geo-las-vegas": "Las Vegas",
"geo-london": "London",
"geo-los-angeles": "Los Angeles",
"geo-madrid": "Madrid",
"geo-melbourne": "Melbourne",
"geo-milan": "Milan",
"geo-montreal": "Montréal",
"geo-mumbai": "Mumbai",
"geo-new-york-city": "New York",
"geo-northern-virginia": "Northern Virginia",
"geo-oregon": "Oregon",
"geo-osaka": "Osaka",
"geo-paris": "Paris",
"geo-queretaro": "Querétaro",
"geo-salt-lake-city": "Salt Lake City",
"geo-san-francisco": "San Francisco",
"geo-santiago": "Santiago",
"geo-sao-paulo": "São Paulo",
"geo-seoul": "Seoul",
"geo-sg": "Singapore",
"geo-south-carolina": "South Carolina",
"geo-st-ghislain": "St Ghislain",
"geo-stockholm": "Stockholm",
"geo-sydney": "Sydney",
"geo-tel-aviv": "Tel Aviv",
"geo-tokyo": "Tokyo",
"geo-toronto": "Toronto",
"geo-turin": "Turin",
"geo-warsaw": "Warsaw",
"geo-zurich": "Zürich",
"key": "Key {keyId}",
Expand Down
15 changes: 15 additions & 0 deletions server_manager/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -123,35 +123,50 @@
"gcp-type-outline-server": "Type 'outline-server' in the 'Name' field.",
"geo-amsterdam": "Amsterdam",
"geo-bangalore": "Bangalore",
"geo-berlin": "Berlin",
"geo-changhua-county": "Changhua County",
"geo-columbus": "Columbus",
"geo-dallas": "Dallas",
"geo-dammam": "Dammam",
"geo-delhi": "Delhi",
"geo-doha": "Doha",
"geo-eemshaven": "Eemshaven",
"geo-frankfurt": "Frankfurt",
"geo-hamina": "Hamina",
"geo-hk": "Hong Kong",
"geo-iowa": "Iowa",
"geo-jakarta": "Jakarta",
"geo-johannesburg": "Johannesburg",
"geo-jurong-west": "Jurong West",
"geo-kuala-lumpur": "Kuala Lumpur",
"geo-las-vegas": "Las Vegas",
"geo-london": "London",
"geo-los-angeles": "Los Angeles",
"geo-madrid": "Madrid",
"geo-melbourne": "Melbourne",
"geo-milan": "Milan",
"geo-montreal": "Montréal",
"geo-mumbai": "Mumbai",
"geo-new-york-city": "New York",
"geo-northern-virginia": "Northern Virginia",
"geo-oregon": "Oregon",
"geo-osaka": "Osaka",
"geo-paris": "Paris",
"geo-queretaro": "Querétaro",
"geo-salt-lake-city": "Salt Lake City",
"geo-san-francisco": "San Francisco",
"geo-santiago": "Santiago",
"geo-sao-paulo": "São Paulo",
"geo-seoul": "Seoul",
"geo-sg": "Singapore",
"geo-south-carolina": "South Carolina",
"geo-st-ghislain": "St. Ghislain",
"geo-stockholm": "Stockholm",
"geo-sydney": "Sydney",
"geo-tel-aviv": "Tel Aviv",
"geo-tokyo": "Tokyo",
"geo-toronto": "Toronto",
"geo-turin": "Turin",
"geo-warsaw": "Warsaw",
"geo-zurich": "Zürich",
"key": "Key {keyId}",
Expand Down
Loading
Loading