The workflow was written for github.com: actions/upload-artifact failed on the Gitea runner, softprops/action-gh-release only talks to the GitHub API, and gen-manifest.js hardcoded github.com URLs. - drop the artifact upload (release assets are the deliverable) - upload release assets with tools/release-gitea.js, a dependency-free Gitea API client; re-running a tag replaces the attachments - gen-manifest.js takes --server/PUBLIC_SERVER_URL and knows that Gitea spells the latest-asset URL /releases/download/latest/<file> - PUBLIC_SERVER_URL is set explicitly because the runner only sees the instance's internal LAN address Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b137bf9eda
commit
bf2daac146
@@ -7,6 +7,11 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
# Public URL of this Gitea instance. The runner is handed the instance's
|
||||||
|
# internal address (a LAN IP), which is no good in a manifest the TV fetches.
|
||||||
|
PUBLIC_SERVER_URL: https://git.crylia.de
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -30,28 +35,31 @@ jobs:
|
|||||||
- name: Generate manifest and repository index
|
- name: Generate manifest and repository index
|
||||||
run: npm run manifest
|
run: npm run manifest
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Show what was built
|
||||||
uses: actions/upload-artifact@v4
|
run: ls -la dist
|
||||||
with:
|
|
||||||
name: lg-update-blocker
|
|
||||||
path: dist/*
|
|
||||||
|
|
||||||
# Tag a commit with v<version> (matching package.json) to publish a
|
# Tag a commit with v<version> (matching package.json) to publish a
|
||||||
# release. The repository URL for the Homebrew Channel stays stable:
|
# release. The repository URL for the Homebrew Channel stays stable:
|
||||||
# https://github.com/<owner>/<repo>/releases/latest/download/apps.json
|
# <server>/<owner>/<repo>/releases/download/latest/apps.json
|
||||||
- name: Release
|
- name: Write release notes
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
uses: softprops/action-gh-release@v2
|
run: |
|
||||||
with:
|
cat > "${RUNNER_TEMP:-/tmp}/release-notes.md" <<EOF
|
||||||
files: dist/*
|
|
||||||
generate_release_notes: true
|
|
||||||
body: |
|
|
||||||
Install through the Homebrew Channel by adding this repository once
|
Install through the Homebrew Channel by adding this repository once
|
||||||
(Homebrew Channel → Settings → Add repository):
|
(Homebrew Channel → Settings → Add repository):
|
||||||
|
|
||||||
```
|
\`\`\`
|
||||||
https://github.com/${{ github.repository }}/releases/latest/download/apps.json
|
${PUBLIC_SERVER_URL}/${GITHUB_REPOSITORY}/releases/download/latest/apps.json
|
||||||
```
|
\`\`\`
|
||||||
|
|
||||||
Or install the `.ipk` below manually with
|
That URL always resolves to the newest release, so future versions
|
||||||
`ares-install <file>.ipk`.
|
show up as updates automatically.
|
||||||
|
|
||||||
|
Or install the \`.ipk\` below manually with \`ares-install <file>.ipk\`.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: node tools/release-gitea.js --notes-from "${RUNNER_TEMP:-/tmp}/release-notes.md"
|
||||||
|
|||||||
@@ -54,12 +54,13 @@ settings from the backup it made.
|
|||||||
|
|
||||||
Add the repository once — *Homebrew Channel → Settings → Add repository*:
|
Add the repository once — *Homebrew Channel → Settings → Add repository*:
|
||||||
|
|
||||||
```
|
```text
|
||||||
https://github.com/<owner>/<repo>/releases/latest/download/apps.json
|
https://git.crylia.de/Crylia/lg_update_blocker/releases/download/latest/apps.json
|
||||||
```
|
```
|
||||||
|
|
||||||
The app then shows up in the Homebrew Channel list, and every new release you
|
The app then shows up in the Homebrew Channel list, and every new release that
|
||||||
tag is picked up automatically (that URL always points at the newest release).
|
is tagged is picked up automatically — Gitea resolves `.../download/latest/...`
|
||||||
|
to the newest release.
|
||||||
|
|
||||||
### Manually
|
### Manually
|
||||||
|
|
||||||
@@ -125,7 +126,8 @@ npm run deploy # ares-install the resulting ipk
|
|||||||
|
|
||||||
## Releasing
|
## Releasing
|
||||||
|
|
||||||
`.github/workflows/release.yml` builds on every push. Tag a commit `v<version>`
|
[.github/workflows/release.yml](.github/workflows/release.yml) builds on every
|
||||||
|
push (Gitea Actions reads `.github/workflows/` too). Tag a commit `v<version>`
|
||||||
(matching `version` in `package.json`) and the workflow additionally publishes a
|
(matching `version` in `package.json`) and the workflow additionally publishes a
|
||||||
release containing:
|
release containing:
|
||||||
|
|
||||||
@@ -135,6 +137,16 @@ release containing:
|
|||||||
* `apps.json` — the one-package repository index used by the repository URL above
|
* `apps.json` — the one-package repository index used by the repository URL above
|
||||||
* `description.html` — the long description shown in the Homebrew Channel
|
* `description.html` — the long description shown in the Homebrew Channel
|
||||||
|
|
||||||
|
Uploading is done by [tools/release-gitea.js](tools/release-gitea.js) against
|
||||||
|
the Gitea API (`softprops/action-gh-release` only speaks to github.com). It is
|
||||||
|
idempotent: re-running a tag reuses the release and replaces the attachments.
|
||||||
|
|
||||||
|
The URLs inside the manifest come from `PUBLIC_SERVER_URL` in the workflow, not
|
||||||
|
from `GITHUB_SERVER_URL` — the runner is handed the instance's internal LAN
|
||||||
|
address, which the TV cannot reach. Change that one variable if the instance
|
||||||
|
moves. Generating for github.com instead works too: `--server` (or
|
||||||
|
`PUBLIC_SERVER_URL`) switches the "latest release asset" URL to GitHub's shape.
|
||||||
|
|
||||||
The app id lives in exactly one place: the `name` field of `package.json`.
|
The app id lives in exactly one place: the `name` field of `package.json`.
|
||||||
Change it there and everything (appinfo, service name, Luna URIs, manifest)
|
Change it there and everything (appinfo, service name, Luna URIs, manifest)
|
||||||
follows.
|
follows.
|
||||||
|
|||||||
+26
-9
@@ -7,12 +7,16 @@
|
|||||||
* can be added under Homebrew Channel ->
|
* can be added under Homebrew Channel ->
|
||||||
* Settings -> Add repository
|
* Settings -> Add repository
|
||||||
*
|
*
|
||||||
* Both are uploaded as release assets, so the stable repository URL is
|
* Both are uploaded as release assets, so the repository URL stays stable
|
||||||
* https://github.com/<owner>/<repo>/releases/latest/download/apps.json
|
* across releases, while the manifest/ipk URLs inside point at the exact
|
||||||
* and the manifest/ipk URLs inside point at the exact tagged release.
|
* tagged release.
|
||||||
*
|
*
|
||||||
* Usage: node tools/gen-manifest.js [--repo owner/repo] [--tag v1.0.0]
|
* Works against GitHub and Gitea/Forgejo; they spell the "latest release
|
||||||
* On GitHub Actions both are picked up from the environment.
|
* asset" URL differently, which is the only thing --server changes.
|
||||||
|
*
|
||||||
|
* Usage: node tools/gen-manifest.js [--server https://host] [--repo owner/repo]
|
||||||
|
* [--tag v1.0.0]
|
||||||
|
* On GitHub/Gitea Actions all three are picked up from the environment.
|
||||||
*/
|
*/
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
@@ -34,6 +38,15 @@ const tag = arg(
|
|||||||
process.env.GITHUB_REF_TYPE === 'tag' ? process.env.GITHUB_REF_NAME : `v${pkg.version}`,
|
process.env.GITHUB_REF_TYPE === 'tag' ? process.env.GITHUB_REF_NAME : `v${pkg.version}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// PUBLIC_SERVER_URL wins over GITHUB_SERVER_URL because a Gitea runner is
|
||||||
|
// handed the instance's internal URL (a LAN address), which is useless in a
|
||||||
|
// manifest the TV has to fetch.
|
||||||
|
const server = arg(
|
||||||
|
'--server',
|
||||||
|
process.env.PUBLIC_SERVER_URL || process.env.GITHUB_SERVER_URL || 'https://github.com',
|
||||||
|
).replace(/\/+$/, '');
|
||||||
|
const isGitHub = /(^|\.)github\.com$/i.test(new URL(server).hostname);
|
||||||
|
|
||||||
const ipkName = `${pkg.name}_${pkg.version}_all.ipk`;
|
const ipkName = `${pkg.name}_${pkg.version}_all.ipk`;
|
||||||
const ipkPath = path.join(root, 'dist', ipkName);
|
const ipkPath = path.join(root, 'dist', ipkName);
|
||||||
if (!fs.existsSync(ipkPath)) {
|
if (!fs.existsSync(ipkPath)) {
|
||||||
@@ -42,8 +55,12 @@ if (!fs.existsSync(ipkPath)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ipk = fs.readFileSync(ipkPath);
|
const ipk = fs.readFileSync(ipkPath);
|
||||||
const releaseBase = repository
|
const releaseBase = repository ? `${server}/${repository}/releases/download/${tag}` : null;
|
||||||
? `https://github.com/${repository}/releases/download/${tag}`
|
// GitHub: /releases/latest/download/<file>, Gitea: /releases/download/latest/<file>
|
||||||
|
const latestBase = repository
|
||||||
|
? isGitHub
|
||||||
|
? `${server}/${repository}/releases/latest/download`
|
||||||
|
: `${server}/${repository}/releases/download/latest`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!releaseBase) {
|
if (!releaseBase) {
|
||||||
@@ -65,7 +82,7 @@ const manifest = {
|
|||||||
title: pkg.title,
|
title: pkg.title,
|
||||||
appDescription: pkg.description,
|
appDescription: pkg.description,
|
||||||
iconUri,
|
iconUri,
|
||||||
sourceUrl: repository ? `https://github.com/${repository}` : undefined,
|
sourceUrl: repository ? `${server}/${repository}` : undefined,
|
||||||
rootRequired: true,
|
rootRequired: true,
|
||||||
ipkUrl: releaseBase ? `${releaseBase}/${ipkName}` : ipkName,
|
ipkUrl: releaseBase ? `${releaseBase}/${ipkName}` : ipkName,
|
||||||
ipkHash: { sha256: crypto.createHash('sha256').update(ipk).digest('hex') },
|
ipkHash: { sha256: crypto.createHash('sha256').update(ipk).digest('hex') },
|
||||||
@@ -99,6 +116,6 @@ console.log(`repository: dist/apps.json`);
|
|||||||
if (repository) {
|
if (repository) {
|
||||||
console.log(
|
console.log(
|
||||||
`\nAdd this in Homebrew Channel -> Settings -> Add repository:\n` +
|
`\nAdd this in Homebrew Channel -> Settings -> Add repository:\n` +
|
||||||
` https://github.com/${repository}/releases/latest/download/apps.json`,
|
` ${latestBase}/apps.json`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Publishes dist/* as attachments on a Gitea/Forgejo release.
|
||||||
|
*
|
||||||
|
* softprops/action-gh-release only talks to the GitHub API, so on a Gitea
|
||||||
|
* runner we do it ourselves. Node 20 has fetch/FormData/Blob built in, so this
|
||||||
|
* needs no dependencies.
|
||||||
|
*
|
||||||
|
* Re-running for the same tag is safe: the release is reused and same-named
|
||||||
|
* attachments are replaced.
|
||||||
|
*
|
||||||
|
* Env (all set automatically by Gitea Actions):
|
||||||
|
* GITHUB_TOKEN / GITEA_TOKEN - token with write access to the repository
|
||||||
|
* GITHUB_API_URL - e.g. https://git.example.com/api/v1
|
||||||
|
* GITHUB_SERVER_URL - fallback for GITHUB_API_URL
|
||||||
|
* GITHUB_REPOSITORY - owner/repo
|
||||||
|
* GITHUB_REF_NAME - the tag, when GITHUB_REF_TYPE is "tag"
|
||||||
|
*
|
||||||
|
* Usage: node tools/release-gitea.js [--api URL] [--repo owner/repo]
|
||||||
|
* [--tag v1.0.0] [--notes-from file]
|
||||||
|
*/
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const root = path.resolve(__dirname, '..');
|
||||||
|
|
||||||
|
function arg(name, fallback) {
|
||||||
|
const index = process.argv.indexOf(name);
|
||||||
|
return index !== -1 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fail(message) {
|
||||||
|
console.error(message);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = process.env.GITHUB_TOKEN || process.env.GITEA_TOKEN || '';
|
||||||
|
const repository = arg('--repo', process.env.GITHUB_REPOSITORY || '');
|
||||||
|
const tag = arg(
|
||||||
|
'--tag',
|
||||||
|
process.env.GITHUB_REF_TYPE === 'tag' ? process.env.GITHUB_REF_NAME : '',
|
||||||
|
);
|
||||||
|
const api = arg(
|
||||||
|
'--api',
|
||||||
|
process.env.GITHUB_API_URL ||
|
||||||
|
(process.env.GITHUB_SERVER_URL ? `${process.env.GITHUB_SERVER_URL}/api/v1` : ''),
|
||||||
|
).replace(/\/+$/, '');
|
||||||
|
|
||||||
|
if (!token) fail('No GITHUB_TOKEN/GITEA_TOKEN in the environment.');
|
||||||
|
if (!repository) fail('No repository - pass --repo owner/repo or set GITHUB_REPOSITORY.');
|
||||||
|
if (!tag) fail('No tag - pass --tag v1.0.0 or run this on a tag push.');
|
||||||
|
if (!api) fail('No API URL - pass --api https://host/api/v1 or set GITHUB_API_URL.');
|
||||||
|
|
||||||
|
const [owner, repo] = repository.split('/');
|
||||||
|
const base = `${api}/repos/${owner}/${repo}`;
|
||||||
|
const notesPath = arg('--notes-from', '');
|
||||||
|
const distDir = path.join(root, 'dist');
|
||||||
|
|
||||||
|
async function call(method, url, { body, headers } = {}) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method,
|
||||||
|
headers: { Authorization: `token ${token}`, ...(headers || {}) },
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${method} ${url} -> ${response.status} ${response.statusText}\n${text}`);
|
||||||
|
}
|
||||||
|
return text ? JSON.parse(text) : {};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findRelease() {
|
||||||
|
const response = await fetch(`${base}/releases/tags/${encodeURIComponent(tag)}`, {
|
||||||
|
headers: { Authorization: `token ${token}` },
|
||||||
|
});
|
||||||
|
if (response.status === 404) return null;
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`lookup of ${tag} -> ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const files = fs.existsSync(distDir)
|
||||||
|
? fs.readdirSync(distDir).filter((name) => fs.statSync(path.join(distDir, name)).isFile())
|
||||||
|
: [];
|
||||||
|
if (!files.length) fail('dist/ is empty - run "npm run dist" first.');
|
||||||
|
|
||||||
|
const notes = notesPath && fs.existsSync(notesPath) ? fs.readFileSync(notesPath, 'utf8') : '';
|
||||||
|
|
||||||
|
let release = await findRelease();
|
||||||
|
if (release) {
|
||||||
|
console.log(`reusing release ${tag} (#${release.id})`);
|
||||||
|
if (notes) {
|
||||||
|
release = await call('PATCH', `${base}/releases/${release.id}`, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ body: notes }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
release = await call('POST', `${base}/releases`, {
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
tag_name: tag,
|
||||||
|
name: tag,
|
||||||
|
body: notes,
|
||||||
|
draft: false,
|
||||||
|
prerelease: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
console.log(`created release ${tag} (#${release.id})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = new Map((release.assets || []).map((asset) => [asset.name, asset.id]));
|
||||||
|
|
||||||
|
for (const name of files) {
|
||||||
|
if (existing.has(name)) {
|
||||||
|
await call('DELETE', `${base}/releases/${release.id}/assets/${existing.get(name)}`);
|
||||||
|
console.log(`replaced ${name}`);
|
||||||
|
}
|
||||||
|
const data = fs.readFileSync(path.join(distDir, name));
|
||||||
|
const form = new FormData();
|
||||||
|
form.append('attachment', new Blob([data], { type: 'application/octet-stream' }), name);
|
||||||
|
await call(
|
||||||
|
'POST',
|
||||||
|
`${base}/releases/${release.id}/assets?name=${encodeURIComponent(name)}`,
|
||||||
|
{ body: form },
|
||||||
|
);
|
||||||
|
console.log(`uploaded ${name} (${(data.length / 1024).toFixed(1)} kB)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n${release.html_url || `${base}/releases/${release.id}`}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => fail(error.message));
|
||||||
Reference in New Issue
Block a user