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
15 changes: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Editor configuration, see http://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
max_line_length = 100

[*.md]
max_line_length = off
trim_trailing_whitespace = false
62 changes: 62 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Every pull request, and the release before it stages anything (release.yml
# calls this workflow): format (Oxfmt), lint (Oxlint), type-check, the unit
# tests and the plugin SDK's contract test kit (checkAuthMethod), the build
# and the tarball's file list; then the end-to-end suite (e2e/run.sh), which
# runs the released QAuth server image with this plugin installed on top,
# against a mock GitHub. pnpm's version is package.json's packageManager.
name: ci

on:
pull_request:
push:
branches: [main]
workflow_call:

permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
check:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
# The Node QAuth's server image runs, and the plugin with it.
node-version: 26
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm run format:check
- run: pnpm run lint
- run: pnpm run typecheck
- name: Unit tests and the SDK's contract test kit (checkAuthMethod)
run: pnpm test
- run: pnpm run build
- name: The files npm would receive
run: |
set -euo pipefail
pnpm pack --pack-destination "$RUNNER_TEMP/pack"
tar -tzf "$RUNNER_TEMP"/pack/*.tgz

e2e:
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 26
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: The released server image plus the plugin, against a mock GitHub
run: bash e2e/run.sh
162 changes: 162 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# The npm release of @qauth-labs/plugin-github, from a tag (RELEASING.md).
#
# A tag vX.Y.Z or vX.Y.Z-rc.N starts it. The tag must name the version in
# package.json. The whole CI runs first (ci.yml: the checks, the contract test
# kit, the end-to-end suite). Then the package is packed once, and that
# tarball, checked by its digest, is staged on npm through trusted publishing
# (OIDC, no token) in the `npm` environment; npm attaches provenance on its
# own. A release candidate goes to the `next` dist-tag, a release to
# `latest`. The staged version goes live only when a maintainer approves it
# on npmjs.com with 2FA. The run leaves a draft GitHub release with the
# tarball and the version's CHANGELOG section.
#
# npm's trusted publisher for @qauth-labs/plugin-github names this file:
# release.yml, environment npm.
name: release

on:
push:
tags: ['v*']

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
ci:
uses: ./.github/workflows/ci.yml

pack:
needs: ci
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.plan.outputs.version }}
dist-tag: ${{ steps.plan.outputs.dist-tag }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# The whole history: the tagged commit must be on main.
fetch-depth: 0
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 26
cache: pnpm
- name: The tag points at a commit on main
run: |
set -euo pipefail
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "The tag points at $GITHUB_SHA, which is not on main: only a reviewed, merged commit is released." >&2
exit 1
fi
- id: plan
name: The tag names the package's version; the dist-tag it goes to
env:
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
version="$(node -p 'require("./package.json").version')"
if [ "$TAG" != "v$version" ]; then
echo "The tag $TAG does not name the package's version $version." >&2
exit 1
fi
case "$version" in
*-rc.*) dist_tag=next ;;
*-*) echo "Only a release candidate (-rc.N) or a release is published: $version." >&2; exit 1 ;;
*) dist_tag=latest ;;
esac
if ! grep -q "^## \[$version\]" CHANGELOG.md; then
echo "CHANGELOG.md has no section for $version." >&2
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "dist-tag=$dist_tag" >> "$GITHUB_OUTPUT"
- run: pnpm install --frozen-lockfile
- run: pnpm run build
- name: Pack it as npm will receive it
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/pack"
pnpm pack --pack-destination "$RUNNER_TEMP/pack"
cd "$RUNNER_TEMP/pack"
tarball="$(ls ./*.tgz)"
sha256sum "$tarball" > "$tarball.sha256"
tar -tzf "$tarball"
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: package
path: ${{ runner.temp }}/pack/
if-no-files-found: error
retention-days: 7

publish:
needs: pack
runs-on: ubuntu-24.04
environment: npm
permissions:
contents: read
id-token: write
env:
DIST_TAG: ${{ needs.pack.outputs.dist-tag }}
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: package
path: pack
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: 26
registry-url: https://registry.npmjs.org
# pnpm builds and packs; npm stages the tarball, as QAuth's release-npm.yml does.
- name: npm with staged publishing through OIDC
run: npm install --global npm@11.21.0
- name: The tarball is the one the pack job made
working-directory: pack
run: sha256sum --check ./*.sha256
- name: Stage it on npm; a maintainer approves it there, with 2FA
working-directory: pack
run: npm stage publish ./*.tgz --tag "$DIST_TAG" --access public

draft-release:
needs: [pack, publish]
runs-on: ubuntu-24.04
permissions:
contents: write
env:
VERSION: ${{ needs.pack.outputs.version }}
DIST_TAG: ${{ needs.pack.outputs.dist-tag }}
TAG: ${{ github.ref_name }}
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: package
path: pack
- name: Draft the release with the tarball and the version's changelog section
run: |
set -euo pipefail
awk -v heading="## [$VERSION]" '
index($0, heading) == 1 { found = 1; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md > "$RUNNER_TEMP/notes.md"
{
echo
echo "---"
echo
echo "Staged on npm as \`$VERSION\` (dist-tag \`$DIST_TAG\`), with provenance. It goes"
echo "live when a maintainer approves the stage on npmjs.com with 2FA; publish this"
echo "draft after that."
} >> "$RUNNER_TEMP/notes.md"
prerelease=()
case "$VERSION" in *-rc.*) prerelease=(--prerelease) ;; esac
gh release create "$TAG" pack/*.tgz pack/*.sha256 \
--draft --verify-tag --title "$TAG" --notes-file "$RUNNER_TEMP/notes.md" "${prerelease[@]}"
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
dist/
coverage/
*.tgz
# The end-to-end suite's generated certificates, allowlist and logs.
e2e/.work/
17 changes: 17 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
// QAuth's own formatting options (qauth-labs/qauth's .oxfmtrc.json).
"semi": true,
"trailingComma": "es5",
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"arrowParens": "always",
"endOfLine": "lf",
"sortPackageJson": false,
// The order of imports, checked by `oxfmt --check` and fixed by `oxfmt`.
"sortImports": true,
// Gitignore-style; .gitignore applies too.
"ignorePatterns": ["node_modules", "dist", "coverage", "*.log", "pnpm-lock.yaml", "e2e/.work"]
}
125 changes: 125 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
// QAuth's own rule set (qauth-labs/qauth's .oxlintrc.json): @eslint/js and
// typescript-eslint `recommended` as Nx configures them, less the Nx module
// boundaries and QAuth's key custody guard, which concern QAuth's own
// workspace. Oxlint's default category is off, so nothing else runs.
// Formatting and the order of imports are Oxfmt's job (.oxfmtrc.json): no
// ESLint plugin, so no ESLint in the tree.
"plugins": ["eslint", "typescript"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["**/coverage/**", "**/dist/**", "**/node_modules/**", "e2e/.work/**"],
"rules": {
"for-direction": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": ["error", { "checkLoops": "allExceptWhileTrue" }],
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-else-if": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-global-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-nonoctal-decimal-escape": "error",
"no-prototype-builtins": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-shadow-restricted-names": ["error", { "reportGlobalThis": true }],
"no-sparse-arrays": "error",
"no-unassigned-vars": "error",
"no-unsafe-finally": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-useless-assignment": "error",
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-useless-escape": "error",
"no-var": "error",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"preserve-caught-error": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error",

"no-array-constructor": "error",
"no-empty-function": "error",
"no-unused-expressions": "error",
"no-unused-vars": [
"warn",
{ "vars": "all", "args": "after-used", "caughtErrors": "all", "ignoreRestSiblings": false }
],
"typescript/adjacent-overload-signatures": "error",
"typescript/ban-ts-comment": "error",
"typescript/no-duplicate-enum-values": "error",
"typescript/no-empty-interface": "error",
"typescript/no-empty-object-type": "error",
"typescript/no-explicit-any": "warn",
"typescript/no-extra-non-null-assertion": "error",
"typescript/no-inferrable-types": "error",
"typescript/no-misused-new": "error",
"typescript/no-namespace": "error",
"typescript/no-non-null-asserted-optional-chain": "error",
"typescript/no-non-null-assertion": "warn",
"typescript/no-this-alias": "error",
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-unsafe-declaration-merging": "error",
"typescript/no-unsafe-function-type": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/prefer-as-const": "error",
"typescript/prefer-namespace-keyword": "error",
"typescript/triple-slash-reference": "error"
},
"overrides": [
{
// Every outbound call goes through the egress client the server hands
// the plugin (ADR-019 Decision 8), never a network API of its own.
"files": ["src/**/*.ts"],
"rules": {
"no-restricted-globals": [
"error",
{
"name": "fetch",
"message": "Use context.egress: every call goes through the egress client."
}
],
"no-restricted-imports": [
"error",
{
"paths": [
{ "name": "node:http", "message": "Use context.egress." },
{ "name": "node:https", "message": "Use context.egress." },
{ "name": "node:net", "message": "Use context.egress." },
{ "name": "node:tls", "message": "Use context.egress." },
{ "name": "node:dgram", "message": "Use context.egress." },
{ "name": "http", "message": "Use context.egress." },
{ "name": "https", "message": "Use context.egress." },
{ "name": "net", "message": "Use context.egress." },
{ "name": "tls", "message": "Use context.egress." },
{ "name": "dgram", "message": "Use context.egress." },
{ "name": "undici", "message": "Use context.egress." }
]
}
]
}
}
]
}
Loading