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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .github/workflows/check-upstream-skills.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
name: Check Upstream Skills

on:
# Run weekly on Monday at 9am UTC
schedule:
- cron: '0 9 * * 1'
# Allow manual trigger
workflow_dispatch:

jobs:
check-upstream:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v5
with:
node-version: 22
cache: npm

- name: Install dependencies
run: npm ci

- name: Check upstream skills for updates
id: check
run: npm run check:upstream-skills
continue-on-error: true

- name: Create or update issue if upstream changed
if: steps.check.outcome == 'failure'
uses: actions/github-script@v7
with:
script: |
const title = 'Upstream skill files have changed';
const body = `The upstream skill check has detected changes in one or more skill files.

Please review the upstream changes and update the local skill files if needed.

**Latest workflow run:** ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}

**After updating:**
1. Review the changes in the upstream repository
2. Update the local SKILL.md file with any relevant changes
3. Update the \`pinned_commit\` in UPSTREAM.json to the new commit hash
`;

// Check if issue already exists
const issues = await github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'upstream-sync'
});

const existingIssue = issues.data.find(issue => issue.title === title);

if (existingIssue) {
// Update existing issue with latest run info
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: existingIssue.number,
body: `Upstream changes still detected. Latest workflow run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
});
console.log(`Updated existing issue #${existingIssue.number}`);
} else {
// Create new issue
const newIssue = await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: title,
body: body,
labels: ['upstream-sync']
});
console.log(`Created new issue #${newIssue.data.number}`);
}

- name: Fail if upstream changed
if: steps.check.outcome == 'failure'
run: exit 1
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"lint:fix": "eslint --fix",
"prettier:check": "prettier --check .",
"prettier:write": "prettier --write .",
"migrate": "migrate"
"migrate": "migrate",
"check:upstream-skills": "tsx scripts/check-upstream-skills.ts"
},
"dependencies": {
"@ai-sdk/openai": "^2.0.80",
Expand Down
153 changes: 153 additions & 0 deletions scripts/check-upstream-skills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/usr/bin/env npx tsx

/**
* Script to check if upstream skill files have been updated.
* Reads upstream-skills.json from the project root and compares
* the pinned content with the current upstream content.
*
* Exit codes:
* 0 - All skills are up to date
* 1 - One or more skills have upstream changes
* 2 - Error occurred during check
*/

import { readFile } from 'node:fs/promises';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = join(__dirname, '..', 'upstream-skills.json');

interface UpstreamConfig {
source_url: string;
pinned_commit: string;
pinned_url: string;
local_path: string;
notes?: string;
}

type UpstreamSkillsConfig = Record<string, UpstreamConfig>;

async function fetchContent(url: string): Promise<string> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(
`Failed to fetch ${url}: ${response.status} ${response.statusText}`,
);
}
return response.text();
}

async function checkSkill(
name: string,
config: UpstreamConfig,
): Promise<{ name: string; changed: boolean; error?: string }> {
try {
// Fetch pinned version
console.log(` Fetching pinned version from: ${config.pinned_url}`);
const pinnedContent = await fetchContent(config.pinned_url);

// Fetch current version from default branch
console.log(` Fetching current version from: ${config.source_url}`);
const currentContent = await fetchContent(config.source_url);

if (pinnedContent !== currentContent) {
return {
name,
changed: true,
};
}

return {
name,
changed: false,
};
} catch (error) {
return {
name,
changed: false,
error: error instanceof Error ? error.message : String(error),
};
}
}

async function main(): Promise<void> {
console.log('Checking upstream skill files for updates...\n');

let config: UpstreamSkillsConfig;
try {
const configContent = await readFile(CONFIG_PATH, 'utf-8');
config = JSON.parse(configContent);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
console.log('No upstream-skills.json found. Nothing to check.');
process.exit(0);
}
throw error;
}

const skillNames = Object.keys(config);
if (skillNames.length === 0) {
console.log('No upstream skills configured. Nothing to check.');
process.exit(0);
}

const results: Array<{ name: string; changed: boolean; error?: string }> = [];

for (const skillName of skillNames) {
console.log(`Checking skill: ${skillName}`);
const result = await checkSkill(skillName, config[skillName]);
results.push(result);
console.log();
}

// Report results
console.log('=== Results ===\n');

const errors = results.filter((r) => r.error);
const changed = results.filter((r) => r.changed && !r.error);
const upToDate = results.filter((r) => !r.changed && !r.error);

if (upToDate.length > 0) {
console.log('Up to date:');
for (const r of upToDate) {
console.log(` - ${r.name}`);
}
console.log();
}

if (errors.length > 0) {
console.log('Errors:');
for (const r of errors) {
console.log(` - ${r.name}: ${r.error}`);
}
console.log();
}

if (changed.length > 0) {
console.log('UPSTREAM CHANGES DETECTED:');
for (const r of changed) {
console.log(` - ${r.name}`);
}
console.log();
console.log(
'Please review the upstream changes and update the local skill files if needed.',
);
console.log(
'After updating, update the pinned_commit in upstream-skills.json to the new commit hash.',
);
process.exit(1);
}

if (errors.length > 0) {
process.exit(2);
}

console.log('All skills are up to date with their upstream sources.');
process.exit(0);
}

main().catch((error) => {
console.error('Fatal error:', error);
process.exit(2);
});
40 changes: 40 additions & 0 deletions skills/postgis/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
name: postgis-skill
description: PostGIS-focused SQL tips, tricks and gotchas. Use when in need of dealing with geospatial data in Postgres.
---

## Style

- PostGIS functions follow their spelling from the manual (`st_segmentize` -> `ST_Segmentize`).
- SQL is lowercase unless instructed otherwise.
- Call geometry column `geom`; geography column `geog`.

## Debugging

- Don't stub stuff out with insane fallbacks (like lat/lon=0) - instead make the rest of the code work around data absence and inform user.
- Check `select postgis_full_version();` to see if all upgrades happened successfully.

## Raster

- Do not work with GDAL on the filesystem. Import things into database and deal with data there.

## PostGIS gotchas

- Do not use geometry typmod unless requested (things like `geometry(multilinestring, 4326)`) - use plain `geometry` or `geography` instead. This removes clutter of `ST_Multi` and errors via `ST_SetSRID`.
- `ST_UnaryUnion(ST_Collect(geom))` is just `ST_Union(geom)`
- `ST_Buffer(geom, 0)` should be `ST_MakeValid(geom)`
- `select min(ST_Distance(..))` should be `select ST_Distance() ... order by a <-> b limit 1` to enable knn gist
- `order by ST_Distance(c.geog, t.geog)` should be `order by c.geog <-> t.geog`
- `ST_UnaryUnion` is a sign you're doing something wrong
- `ST_MakeValid` is a sign you're doing something wrong on the previous step
- be extra attintive when calling `ST_SetSRID`: check the actual projection of input data, check if it can be set correctly during input (`ST_GeomFromGeoJSON`, `EWKT`-style `SRID=4326;POINT(...`, `EWKB` allow that). Check if `ST_Transform` is needed instead.
- when looking for relation between point and polygon, prefer `ST_Intersects` to other topology predicates
- when generating complex geometry by walking raster or grid, may make sense to `ST_Simplify(geom, 0)`
- to generate neighbourhoods of predictable size, use `ST_ClusterKMeans` with k=2 and `max_radius` set to your distance.
- use `ST_AsEWKB` for binary representation instead of `ST_AsWKB` to keep SRID.
- Choosing projection:
SRID=4326 (2D longlat) when input or output is longitude and latitude and coordinate value is to be shown to user.
SRID=3857 (2D Spherical Mercator) when output will be shown on web map, ST_AsMVT, or 2D KNN requests of short distance are to be executed.
SRID=4978 (3D XYZ) when performing internal computations, line-of-sight, clustering and averaging across antimeridian. Beware: only use 3D-aware operations, ST_Force3DZ on 2D CRS data before calling ST_Transform to it.
- Instead of using `ST_Hexagon` / `ST_HexagonGrid` use `h3` extension.
- When you know the data is going to be dumped in binary form, gzipped and moved around, consider using `ST_QuantizeCoordinates` if precision is known.
11 changes: 11 additions & 0 deletions upstream-skills.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"postgis": {
"source_url": "https://gitea.osgeo.org/postgis/postgis/raw/branch/master/doc/SKILL.md",
"pinned_commit": "2cdf739b02ec00ff6b784e15ed8d3d87bf3f788f",
"pinned_url": "https://gitea.osgeo.org/postgis/postgis/raw/commit/2cdf739b02ec00ff6b784e15ed8d3d87bf3f788f/doc/SKILL.md",
"local_path": "skills/postgis/SKILL.md",
"author": "Darafei Praliaskouski",
"author_url": "https://gitea.osgeo.org/komzpa",
"thanks": "Thank you to Darafei Praliaskouski for creating and maintaining the PostGIS SKILL.md guide."
}
}