diff --git a/.gitea/workflows/publish.yml b/.gitea/workflows/publish.yml index 167aa08..d69aa9e 100644 --- a/.gitea/workflows/publish.yml +++ b/.gitea/workflows/publish.yml @@ -141,3 +141,100 @@ jobs: - name: Log out if: always() && github.event_name != 'pull_request' run: docker logout "${{ vars.REGISTRY }}" || true + + # Once a release is out, the version in package.json has already shipped. + # Moving it on to the next patch means the working tree is never sitting on + # a number that is published and immutable, and that a build from main is + # always identifiable as "after 1.2.0" rather than "1.2.0, but not really". + # + # `needs: build` is the point of putting this here rather than in a workflow + # of its own: a version that failed to publish has not been released, and + # bumping past it would say it had. + bump: + name: Move the working version on + needs: build + # Tags only, and only final ones. A prerelease has not shipped the version + # it is a candidate for, so there is nothing yet to move past. + if: github.ref_type == 'tag' && !contains(github.ref_name, '-') + runs-on: ubuntu-latest + container: + image: node:22 + steps: + # The tag names a commit in main's history, but the bump belongs on the + # branch, so this checks out main rather than the tag. + - uses: actions/checkout@v4 + with: + ref: main + # The task token can push only if the instance allows Actions to + # write to the repository. Where it does not, set VERSION_BUMP_TOKEN + # to a personal access token with write access and it is used + # instead. + token: ${{ secrets.VERSION_BUMP_TOKEN || secrets.GITEA_TOKEN }} + + - name: Work out the next patch version + id: next + run: | + set -euo pipefail + + version="${{ github.ref_name }}" + version="${version#v}" + + major="${version%%.*}" + rest="${version#*.}" + minor="${rest%%.*}" + patch="${rest##*.}" + + # `10#` forces base ten: a patch number written 08 would otherwise be + # read as octal and fail to parse. + next="${major}.${minor}.$((10#${patch} + 1))" + + echo "next=${next}" >> "$GITHUB_OUTPUT" + echo "Released ${version}; the working version becomes ${next}" + + - name: Bump package.json + id: bump + env: + NEXT: ${{ steps.next.outputs.next }} + run: | + set -euo pipefail + + current="$(node -p "require('./package.json').version")" + if [ "${current}" = "${NEXT}" ]; then + echo "package.json is already ${NEXT}; nothing to do." + echo "changed=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # npm rather than editing the file: the version is in the lockfile + # too, in more than one place, and they have to agree. + npm version "${NEXT}" --no-git-tag-version --allow-same-version >/dev/null + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "package.json ${current} -> ${NEXT}" + + - name: Commit it to main + if: steps.bump.outputs.changed == 'true' + env: + NEXT: ${{ steps.next.outputs.next }} + run: | + set -euo pipefail + + # A name that is not a person, and a reserved address that can never + # resolve to one. Nothing here names the instance it runs on. + git config user.name 'Release bot' + git config user.email 'release-bot@noreply.invalid' + + git add package.json package-lock.json + # `[skip ci]` because this commit is a number and nothing else: + # without it the push to main starts another build of the very image + # that was just published. + git commit -m "Set the working version to ${NEXT} [skip ci]" + + if ! git push origin HEAD:main; then + echo >&2 + echo "Could not push the version bump to main. Either the Actions" >&2 + echo "token has no write access to this repository, or main is" >&2 + echo "protected against direct pushes. Set VERSION_BUMP_TOKEN to a" >&2 + echo "token that may push to main, or allow that token past the" >&2 + echo "branch protection." >&2 + exit 1 + fi diff --git a/CLAUDE.md b/CLAUDE.md index 3603d7a..c39cd82 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -201,6 +201,17 @@ The registry comes from the `REGISTRY` repository variable, the image name from `IMAGE_NAME` or the repository name, and credentials from `REGISTRY_USER` and the `REGISTRY_TOKEN` secret. Nothing about any particular deployment is committed here. +A release also moves `package.json` on to the next patch version, committed to main by +the `bump` job — so the number in the tree is never one that has already shipped and +been made immutable. It lives in `publish.yml` rather than a workflow of its own so it +can say `needs: build`: a version that failed to publish has not been released, and +bumping past it would claim otherwise. Prereleases are skipped, being candidates for a +version that has not shipped. The bump goes through `npm version` rather than an edit in +place, because the version is in the lockfile too, in more than one place, and the two +have to agree. The commit carries `[skip ci]`, or pushing it would rebuild the image +that was just published. Pushing to main needs a token with write access — +`VERSION_BUMP_TOKEN` overrides the task token where that one cannot. + Two things any deployment has to get right, both learned the hard way: - **Chromium needs more than the default 64Mi `/dev/shm`** or it crashes. Mount a diff --git a/README.md b/README.md index 69450f7..4bdcf9c 100644 --- a/README.md +++ b/README.md @@ -21,15 +21,43 @@ readable in your history. Replace `antisocial.example.com` with wherever you are running it. -| Platform | Find | Replace | -| --------- | ------------------------------------------------------------- | ------------------------------------------- | -| X | `/^https:\/\/(?:www\.\|mobile\.)?(?:x\|twitter)\.com\/(.*)$/` | `https://antisocial.example.com/x/$1` | -| Threads | `/^https:\/\/(?:www\.)?threads\.(?:net\|com)\/(.*)$/` | `https://antisocial.example.com/threads/$1` | -| Instagram | `/^https:\/\/(?:www\.)?instagram\.com\/(.*)$/` | `https://antisocial.example.com/ig/$1` | -| TikTok | `/^https:\/\/(?:www\.\|vm\.\|vt\.)?tiktok\.com\/(.*)$/` | `https://antisocial.example.com/tiktok/$1` | -| Bluesky | `/^https:\/\/bsky\.app\/(.*)$/` | `https://antisocial.example.com/bsky/$1` | -| Reddit | `/^https:\/\/(?:www\.\|old\.\|new\.\|np\.\|m\.)?reddit\.com\/(.*)$/` | `https://antisocial.example.com/reddit/$1` | -| Reddit | `/^https:\/\/redd\.it\/(.*)$/` | `https://antisocial.example.com/reddit/$1` | +Each rule is two fields. Both are on their own line below, and neither needs any +escaping — copy them straight out of this file. + +```text +# X +/^https:\/\/(?:www\.|mobile\.)?(?:x|twitter)\.com\/(.*)$/ +https://antisocial.example.com/x/$1 + +# Threads +/^https:\/\/(?:www\.)?threads\.(?:net|com)\/(.*)$/ +https://antisocial.example.com/threads/$1 + +# Instagram +/^https:\/\/(?:www\.)?instagram\.com\/(.*)$/ +https://antisocial.example.com/ig/$1 + +# TikTok +/^https:\/\/(?:www\.|vm\.|vt\.)?tiktok\.com\/(.*)$/ +https://antisocial.example.com/tiktok/$1 + +# Bluesky +/^https:\/\/bsky\.app\/(.*)$/ +https://antisocial.example.com/bsky/$1 + +# Reddit +/^https:\/\/(?:www\.|old\.|new\.|np\.|m\.)?reddit\.com\/(.*)$/ +https://antisocial.example.com/reddit/$1 + +# Reddit short links +/^https:\/\/redd\.it\/(.*)$/ +https://antisocial.example.com/reddit/$1 +``` + +A code block rather than a table, because a table cell cannot hold a bare `|` — it has +to be written `\|`, which renders correctly and copies wrongly. The alternation in these +rules is full of them, and a regex whose pipes arrive as literal pipes matches nothing +and says nothing about why. So `https://x.com/user/status/123` becomes `https://antisocial.example.com/x/user/status/123`. @@ -42,7 +70,8 @@ where it came from Reddit. A Reddit `/r//s/` share link is followed t post it points at, and that permalink — not the opaque share code — is what the copy button hands back. -`/` serves this table with the live hostnames, if you'd rather read it there. +`/` serves these rules with the live hostname already filled in, if you'd rather copy +them from there. ## How it works @@ -59,13 +88,13 @@ Each adapter layers its extraction, most structured first: 3. **The rendered DOM** — whatever is actually on screen is real. 4. **Open Graph tags** — the floor, and enough to show something. -| Platform | Loads | Reads | -| --------- | ---------------------------- | -------------------------------------------------------- | -| Bluesky | the public AT Protocol API | `getPostThread`; falls back to the post page | -| X | `platform.twitter.com` embed | the `cdn.syndication.twimg.com/tweet-result` response | -| Instagram | `/embed/captioned/` | `shortcode_media`, then the rendered `