Skip to content
Merged
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
58 changes: 58 additions & 0 deletions .github/workflows/logo-drift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: logo drift

on:
schedule:
- cron: "17 6 * * 1"
workflow_dispatch:
pull_request:
paths:
- "profile/assets/icons/**"
- "profile/README.md"

permissions:
contents: read

jobs:
check:
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- id: check
run: node profile/assets/icons/check-sources.mjs 2>&1 | tee report.txt
continue-on-error: true

# On a pull request the failed step is visible in the checks, so leave it
# there. On the schedule nobody is looking, and the fix is easy to forget,
# so it needs somewhere durable to sit.
- if: steps.check.outcome == 'failure' && github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
existing=$(gh issue list --state open --label logo-drift --limit 1 --json number --jq '.[0].number // ""')
body=$(printf 'A logo on the org profile no longer matches the project it was copied from.\n\n```\n%s\n```\n\n%s\n' "$(cat report.txt)" "$RUN_URL")
if [ -n "$existing" ]; then
gh issue comment "$existing" --body "$body"
else
gh label create logo-drift --description "Copied project logos are out of date" --color d4c5f9 --force
gh issue create --title "Project logos are out of date" --label logo-drift --body "$body"
fi

# Close the issue once the copies are back in step, so an open one always
# means there is something to do.
- if: steps.check.outcome == 'success' && github.event_name != 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
for n in $(gh issue list --state open --label logo-drift --json number --jq '.[].number'); do
gh issue close "$n" --comment "Logos match their sources again."
done

- if: steps.check.outcome == 'failure'
run: exit 1
2 changes: 2 additions & 0 deletions profile/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ different layer of the stack.
| <img src="./assets/icons/ink.svg" width="20" alt=""> | **[ink](https://github.com/Ad-Astra-Computing/ink)** | An open protocol for signed agent-to-agent messages. The receiver verifies the envelope before it acts. |
| <img src="./assets/icons/ahd.svg" width="20" alt=""> | **[ahd](https://github.com/Ad-Astra-Computing/ahd)** | Critiques of AI-generated design that anyone can re-run. Findings stay reproducible against a named, versioned rule set. |
| <img src="./assets/icons/nixpkgs.svg" width="20" alt=""> | **[nixpkgs](https://github.com/Ad-Astra-Computing/nixpkgs)** | The working fork for Trace, our research on AI-assisted CVE remediation. Nothing here is submitted upstream. |
| <img src="./assets/icons/aer.svg" width="20" alt=""> | **[AER](https://aer.adastra.computer)** | A flight recorder for AI agents. Every run leaves a signed execution record, so what an agent did stays answerable after the fact. |
| <img src="./assets/icons/folio.svg" width="20" alt=""> | **[Folio](https://receiptsofthought.com)** | A desktop writing app that keeps a tamper-evident record of how a piece was made. It emits open [receipts](https://receiptsofthought.com) anyone can verify. |

### Also here

Expand Down
6 changes: 6 additions & 0 deletions profile/assets/icons/aer.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
94 changes: 94 additions & 0 deletions profile/assets/icons/check-sources.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Each icon here is a copy of a mark owned by another project. This checks the
// copies against their sources so a redrawn mark does not sit stale on the org
// profile indefinitely. It never writes: on a mismatch it reports which file to
// re-copy from where, and exits non-zero.
//
// Icons with no reachable source are declared `unchecked` in sources.json and
// reported as such. A missing entry is an error, so adding an icon without
// deciding how it gets watched fails here rather than passing quietly.
import { readFile, readdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const HERE = dirname(fileURLToPath(import.meta.url));
const sources = JSON.parse(await readFile(join(HERE, "sources.json"), "utf8"));

async function fetchText(url) {
// One retry, so a single 5xx on the weekly run does not read as drift.
let last;
for (let attempt = 0; attempt < 2; attempt++) {
if (attempt > 0) await new Promise((r) => setTimeout(r, 3000));
try {
const res = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(20000) });
if (!res.ok) {
last = new Error(`HTTP ${res.status}`);
continue;
}
return await res.text();
} catch (err) {
last = err;
}
}
throw last;
}

const problems = [];

const icons = (await readdir(HERE)).filter((f) => f.endsWith(".svg")).map((f) => f.slice(0, -4)).sort();

for (const name of icons) {
const spec = sources[name];
if (spec === undefined) {
problems.push(`${name}: no entry in sources.json. Add one, or mark it "unchecked" with a reason.`);
continue;
}

if (spec.mode === "unchecked") {
console.log(`skipped ${name}: ${spec.reason}`);
continue;
}

const local = await readFile(join(HERE, `${name}.svg`), "utf8");

let upstream;
try {
upstream = await fetchText(spec.url);
} catch (err) {
// An unreachable source is its own problem: the check stops being able to
// answer whether the copy is current, which is the only thing it is for.
problems.push(`${name}: source unreachable, ${spec.url} (${err.message})`);
continue;
}

if (spec.mode === "bytes") {
if (local !== upstream) {
problems.push(
`${name}: ${name}.svg no longer matches ${spec.url}. Re-copy it, check it still reads at 20px on both GitHub themes, then commit.`,
);
}
} else if (spec.mode === "contains") {
if (!upstream.includes(spec.marker)) {
problems.push(
`${name}: ${spec.url} no longer contains the marker this copy was taken from. The mark was probably redrawn, so re-derive ${name}.svg from it.`,
);
}
} else {
problems.push(`${name}: unknown mode ${JSON.stringify(spec.mode)} in sources.json`);
}

console.log(`checked ${name} against ${spec.url}`);
}

for (const name of Object.keys(sources)) {
if (!icons.includes(name)) {
problems.push(`${name}: listed in sources.json but ${name}.svg does not exist.`);
}
}

if (problems.length > 0) {
console.error(`\n${problems.length} problem(s):`);
for (const p of problems) console.error(` ${p}`);
process.exit(1);
}

console.log(`\nall ${icons.length} icons accounted for`);
18 changes: 18 additions & 0 deletions profile/assets/icons/folio.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
28 changes: 28 additions & 0 deletions profile/assets/icons/sources.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"vega-agent": {
"url": "https://raw.githubusercontent.com/Ad-Astra-Computing/vega-agent/main/.github/assets/vega-logo-light.svg",
"mode": "contains",
"marker": "M50 2 L58 42 L98 50 L58 58 L50 98 L42 58 L2 50 L42 42 Z",
"note": "vega-agent publishes only the large gradient logo, not the favicon this copy came from. The star outline is the part both files share, so this watches the path data instead of the whole file."
},
"ink": {
"url": "https://ink.tulpa.network/favicon.svg",
"mode": "bytes"
},
"ahd": {
"url": "https://ahd.adastra.computer/favicon.svg",
"mode": "bytes"
},
"nixpkgs": {
"url": "https://raw.githubusercontent.com/NixOS/nixos-artwork/master/logo/nix-snowflake-colours.svg",
"mode": "bytes"
},
"aer": {
"url": "https://aer.adastra.computer/favicon.svg",
"mode": "bytes"
},
"folio": {
"mode": "unchecked",
"reason": "Folio has no public repo or site, so there is no source this workflow can reach. Copied by hand from assets/logo-small.svg, the variant drawn for sub-64px use. Re-copy it when Folio gets a public home, and give it a url here."
}
}