Skip to content
Merged

dev #47

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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
44 changes: 0 additions & 44 deletions .all-contributorsrc

This file was deleted.

76 changes: 76 additions & 0 deletions .github/workflows/contributors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Contributors

on:
pull_request:
branches: [dev, main]
push:
branches: [main]
paths:
- .vitepress/data/contribution-types.json
- .vitepress/data/contribution-types.schema.json
- .vitepress/data/contributors.json
- .vitepress/data/contributors.schema.json
- scripts/contributors.mjs
- scripts/contributors.test.mjs
- README.md
- .github/workflows/contributors.yml
- .github/workflows/reconcile-contributors.yml
schedule:
- cron: "41 3 * * *"
workflow_dispatch:
inputs:
dry_run:
description: Render and report without creating pull requests
required: true
default: true
type: boolean

permissions:
contents: read

jobs:
check-source:
name: Check canonical contributor data
runs-on: ubuntu-latest
steps:
- name: Reject unsafe manual reconciliation
if: >-
github.event_name == 'workflow_dispatch' &&
!inputs.dry_run &&
(github.ref != 'refs/heads/main' || vars.CONTRIBUTOR_SYNC_ENABLED != 'true')
run: |
echo "::error::Write mode requires main and CONTRIBUTOR_SYNC_ENABLED=true"
exit 1

- name: Checkout canonical source
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Validate registry, tests, and generated README
run: npm run check:contributors

reconcile-targets:
name: ${{ matrix.repository }}
needs: check-source
if: >-
github.event_name != 'pull_request' &&
(inputs.dry_run ||
(github.ref == 'refs/heads/main' && vars.CONTRIBUTOR_SYNC_ENABLED == 'true'))
strategy:
fail-fast: false
matrix:
repository:
- textmode.js-dev
- textmode.synth.js
- textmode.export.js
- textmode.figlet.js
- textmode.filters.js
uses: ./.github/workflows/reconcile-contributors.yml
with:
app-client-id: ${{ vars.CONTRIBUTOR_SYNC_APP_CLIENT_ID }}
mode: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run && 'dry-run' || 'reconcile' }}
source-ref: ${{ github.sha }}
target-repository: ${{ matrix.repository }}
secrets:
app-private-key: ${{ secrets.CONTRIBUTOR_SYNC_APP_PRIVATE_KEY }}
10 changes: 5 additions & 5 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v4
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 20
cache: npm

- name: Setup Pages
uses: actions/configure-pages@v4
uses: actions/configure-pages@1f0c5cde4bc74cd7e1254d0cb4de8d49e9068c7d # v4

- name: Install dependencies
run: npm install --frozen-lockfile
Expand All @@ -39,7 +39,7 @@ jobs:
run: npm run build

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3
with:
path: .vitepress/dist

Expand All @@ -53,4 +53,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
206 changes: 206 additions & 0 deletions .github/workflows/reconcile-contributors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
name: Reconcile contributor README

on:
workflow_call:
inputs:
app-client-id:
description: Client ID for textmode-contributors-bot
required: true
type: string
mode:
description: Operation to perform (dry-run or reconcile)
required: true
type: string
source-ref:
description: Full code.textmode.art commit SHA containing the canonical data
required: true
type: string
target-repository:
description: Allowlisted repository name to update
required: true
type: string
secrets:
app-private-key:
description: Private key for textmode-contributors-bot
required: true

permissions:
contents: read

jobs:
reconcile:
name: ${{ inputs.mode }} ${{ inputs.target-repository }}/dev
runs-on: ubuntu-latest
concurrency:
group: contributors-${{ inputs.target-repository }}-dev
cancel-in-progress: false

steps:
- name: Validate workflow inputs
env:
APP_CLIENT_ID: ${{ inputs.app-client-id }}
APP_PRIVATE_KEY: ${{ secrets.app-private-key }}
MODE: ${{ inputs.mode }}
SOURCE_REF: ${{ inputs.source-ref }}
TARGET_REPOSITORY: ${{ inputs.target-repository }}
run: |
case "$MODE" in
dry-run|reconcile) ;;
*) echo "::error::Unsupported reconciliation mode: $MODE"; exit 1 ;;
esac

case "$TARGET_REPOSITORY" in
textmode.js-dev|textmode.synth.js|textmode.export.js|textmode.figlet.js|textmode.filters.js) ;;
*) echo "::error::Unsupported target repository: $TARGET_REPOSITORY"; exit 1 ;;
esac

if [[ ! "$SOURCE_REF" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::source-ref must be a full commit SHA"
exit 1
fi

if [[ -z "$APP_CLIENT_ID" || -z "$APP_PRIVATE_KEY" ]]; then
echo "::error::Contributor synchronization GitHub App credentials are not configured"
exit 1
fi

- name: Create short-lived target token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
client-id: ${{ inputs.app-client-id }}
private-key: ${{ secrets.app-private-key }}
owner: humanbydefinition
repositories: ${{ inputs.target-repository }}
permission-contents: ${{ inputs.mode == 'reconcile' && 'write' || 'read' }}
permission-pull-requests: ${{ inputs.mode == 'reconcile' && 'write' || 'read' }}

- name: Verify target dev branch
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
TARGET_REPOSITORY: ${{ inputs.target-repository }}
run: gh api "repos/humanbydefinition/$TARGET_REPOSITORY/branches/dev" --silent

- name: Checkout target dev branch
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: humanbydefinition/${{ inputs.target-repository }}
ref: dev
fetch-depth: 0
path: target
token: ${{ steps.app-token.outputs.token }}

- name: Checkout canonical contributor source
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: humanbydefinition/code.textmode.art
ref: ${{ inputs.source-ref }}
sparse-checkout: |
.vitepress/data/contribution-types.json
.vitepress/data/contributors.json
scripts/contributors.mjs
sparse-checkout-cone-mode: false
path: contributor-source
persist-credentials: false

- name: Resolve canonical source
id: source
working-directory: contributor-source
env:
EXPECTED_SOURCE_SHA: ${{ inputs.source-ref }}
run: |
SOURCE_SHA="$(git rev-parse HEAD)"
if [[ "$SOURCE_SHA" != "$EXPECTED_SOURCE_SHA" ]]; then
echo "::error::Canonical source resolved to $SOURCE_SHA instead of $EXPECTED_SOURCE_SHA"
exit 1
fi
echo "sha=$SOURCE_SHA" >> "$GITHUB_OUTPUT"

- name: Render canonical section
run: |
node contributor-source/scripts/contributors.mjs render \
--registry contributor-source/.vitepress/data/contributors.json \
--types contributor-source/.vitepress/data/contribution-types.json \
--readme target/README.md

- name: Inspect generated diff
id: diff
working-directory: target
run: |
if git diff --quiet -- README.md; then
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi

CHANGED_FILES="$(git diff --name-only)"
if [[ "$CHANGED_FILES" != "README.md" ]]; then
echo "::error::Contributor renderer changed files other than README.md"
git diff --name-status
exit 1
fi

echo "changed=true" >> "$GITHUB_OUTPUT"

- name: Show dry-run diff
if: inputs.mode == 'dry-run' && steps.diff.outputs.changed == 'true'
working-directory: target
run: git diff -- README.md

- name: Create or refresh synchronization pull request
if: inputs.mode == 'reconcile' && steps.diff.outputs.changed == 'true'
working-directory: target
env:
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
SOURCE_SHA: ${{ steps.source.outputs.sha }}
TARGET_REPOSITORY: ${{ inputs.target-repository }}
WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
FULL_REPOSITORY="humanbydefinition/$TARGET_REPOSITORY"
SYNC_BRANCH="automation/sync-contributors/dev"
BOT_USER_ID="$(gh api "/users/$APP_SLUG[bot]" --jq .id)"

git config user.name "$APP_SLUG[bot]"
git config user.email "$BOT_USER_ID+$APP_SLUG[bot]@users.noreply.github.com"
git switch -C "$SYNC_BRANCH"
git add README.md
git commit -m "docs: sync ecosystem contributors"

if git ls-remote --exit-code --heads origin "$SYNC_BRANCH" >/dev/null 2>&1; then
git fetch origin "refs/heads/$SYNC_BRANCH:refs/remotes/origin/$SYNC_BRANCH"
fi
git push --force-with-lease origin "HEAD:refs/heads/$SYNC_BRANCH"

PR_NUMBER="$(gh pr list \
--repo "$FULL_REPOSITORY" \
--base dev \
--head "$SYNC_BRANCH" \
--state open \
--json number \
--jq '.[0].number // empty')"

printf -v PR_BODY \
'Generated from humanbydefinition/code.textmode.art@%s.\n\nThis pull request changes only the canonical README Contributors section.\n\nReconciliation run: %s' \
"$SOURCE_SHA" \
"$WORKFLOW_URL"

if [[ -z "$PR_NUMBER" ]]; then
PR_URL="$(gh pr create \
--repo "$FULL_REPOSITORY" \
--base dev \
--head "$SYNC_BRANCH" \
--title "docs: sync ecosystem contributors" \
--body "$PR_BODY")"
PR_NUMBER="${PR_URL##*/}"
else
gh pr edit "$PR_NUMBER" \
--repo "$FULL_REPOSITORY" \
--title "docs: sync ecosystem contributors" \
--body "$PR_BODY"
fi

gh pr merge "$PR_NUMBER" --repo "$FULL_REPOSITORY" --auto --squash

- name: Report no-op
if: steps.diff.outputs.changed == 'false'
run: echo "${{ inputs.target-repository }}/dev already matches code.textmode.art@${{ steps.source.outputs.sha }}"
Loading