-
Notifications
You must be signed in to change notification settings - Fork 0
Add post-deployment X publisher #331
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,199 @@ | ||
| name: Publish New Learning Content to X | ||
|
|
||
| on: | ||
| workflow_call: | ||
| inputs: | ||
| deployed_revision: | ||
| description: Commit successfully deployed by the calling documentation workflow | ||
| required: true | ||
| type: string | ||
|
|
||
| workflow_dispatch: | ||
| inputs: | ||
| dry_run: | ||
| description: Show eligible publications without calling X or changing state | ||
| required: true | ||
| type: boolean | ||
| default: true | ||
|
|
||
| permissions: {} | ||
|
|
||
| concurrency: | ||
| group: publish-learning-to-x | ||
| cancel-in-progress: false | ||
|
|
||
| env: | ||
| DOTNET_NOLOGO: true | ||
| DOTNET_CLI_TELEMETRY_OPTOUT: true | ||
| STATE_BRANCH: automation/x-publisher-state | ||
| STATE_FILE: x-publisher-state.json | ||
| DEPLOYED_REVISION_URL: https://asibackbone.github.io/Learning/deployment-revision.txt | ||
| DEPLOYED_SITEMAP_URL: https://asibackbone.github.io/Learning/sitemap.xml | ||
|
|
||
| jobs: | ||
| dry-run: | ||
| name: Preview X publications | ||
| if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }} | ||
| runs-on: ubuntu-latest | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| steps: | ||
| - name: Checkout repository without credentials | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| persist-credentials: false | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Setup .NET SDK | ||
| uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | ||
| with: | ||
| global-json-file: global.json | ||
|
|
||
| - name: Load deployed revision, sitemap, and state | ||
| id: publication | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| deployed_revision="$(curl --fail --silent --show-error --location --max-time 20 "$DEPLOYED_REVISION_URL")" | ||
| deployed_revision="${deployed_revision//$'\r'/}" | ||
| deployed_revision="${deployed_revision//$'\n'/}" | ||
| git cat-file -e "${deployed_revision}^{commit}" | ||
| git merge-base --is-ancestor "$deployed_revision" origin/main | ||
|
|
||
| curl --fail --silent --show-error --location --max-time 20 \ | ||
| --output "$RUNNER_TEMP/deployed-sitemap.xml" \ | ||
| "$DEPLOYED_SITEMAP_URL" | ||
|
|
||
| if git fetch --no-tags origin "refs/heads/$STATE_BRANCH:refs/remotes/origin/$STATE_BRANCH"; then | ||
| git show "origin/$STATE_BRANCH:$STATE_FILE" > "$RUNNER_TEMP/$STATE_FILE" | ||
| fi | ||
|
|
||
| echo "deployed_revision=$deployed_revision" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Preview publications without X credentials | ||
| env: | ||
| DEPLOYED_REVISION: ${{ steps.publication.outputs.deployed_revision }} | ||
| run: >- | ||
| dotnet run --file tools/publish-x.cs -- | ||
| --head "$DEPLOYED_REVISION" | ||
| --sitemap "$RUNNER_TEMP/deployed-sitemap.xml" | ||
| --state "$RUNNER_TEMP/$STATE_FILE" | ||
| --account jackdaw-patio | ||
| --dry-run | ||
|
|
||
| publish: | ||
| name: Publish eligible content to X | ||
| if: >- | ||
| ${{ | ||
| github.event_name != 'workflow_dispatch' || | ||
| (github.event_name == 'workflow_dispatch' && !inputs.dry_run) | ||
| }} | ||
| runs-on: ubuntu-latest | ||
| environment: jackdaw-patio-x | ||
|
|
||
| permissions: | ||
| contents: write | ||
|
|
||
| steps: | ||
| - name: Checkout trusted default branch | ||
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: main | ||
| persist-credentials: false | ||
| fetch-depth: 0 | ||
|
|
||
| - name: Setup .NET SDK | ||
| uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0 | ||
| with: | ||
| global-json-file: global.json | ||
|
|
||
| - name: Resolve deployed publication boundary | ||
| id: publication | ||
| shell: bash | ||
| env: | ||
| EXPECTED_REVISION: ${{ inputs.deployed_revision }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| deployed_revision="" | ||
| for attempt in 1 2 3 4 5 6; do | ||
| deployed_revision="$(curl --fail --silent --show-error --location --max-time 20 "$DEPLOYED_REVISION_URL")" | ||
| deployed_revision="${deployed_revision//$'\r'/}" | ||
| deployed_revision="${deployed_revision//$'\n'/}" | ||
|
|
||
| if [[ -z "${EXPECTED_REVISION:-}" || "$deployed_revision" == "$EXPECTED_REVISION" ]]; then | ||
| break | ||
| fi | ||
|
|
||
| if [[ "$attempt" == "6" ]]; then | ||
| echo "Published revision '$deployed_revision' did not reach expected revision '$EXPECTED_REVISION'." >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| sleep 10 | ||
| done | ||
|
|
||
| git cat-file -e "${deployed_revision}^{commit}" | ||
| git merge-base --is-ancestor "$deployed_revision" origin/main | ||
| curl --fail --silent --show-error --location --max-time 20 \ | ||
| --output "$RUNNER_TEMP/deployed-sitemap.xml" \ | ||
| "$DEPLOYED_SITEMAP_URL" | ||
| echo "deployed_revision=$deployed_revision" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Prepare durable publisher state worktree | ||
| id: state | ||
| shell: bash | ||
| env: | ||
| DEPLOYED_REVISION: ${{ steps.publication.outputs.deployed_revision }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| state_directory="$RUNNER_TEMP/x-publisher-state" | ||
| if git fetch --no-tags origin "refs/heads/$STATE_BRANCH:refs/remotes/origin/$STATE_BRANCH"; then | ||
| git worktree add --detach "$state_directory" "origin/$STATE_BRANCH" | ||
| else | ||
| git worktree add --detach "$state_directory" "$DEPLOYED_REVISION" | ||
| fi | ||
|
|
||
| echo "directory=$state_directory" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Publish and record receipts | ||
| env: | ||
| X_API_KEY: ${{ secrets.X_API_KEY }} | ||
| X_API_KEY_SECRET: ${{ secrets.X_API_KEY_SECRET }} | ||
| X_ACCESS_TOKEN: ${{ secrets.X_ACCESS_TOKEN }} | ||
| X_ACCESS_TOKEN_SECRET: ${{ secrets.X_ACCESS_TOKEN_SECRET }} | ||
| X_ACCOUNT_USER_ID: ${{ vars.X_ACCOUNT_USER_ID }} | ||
| DEPLOYED_REVISION: ${{ steps.publication.outputs.deployed_revision }} | ||
| STATE_DIRECTORY: ${{ steps.state.outputs.directory }} | ||
| run: >- | ||
| dotnet run --file tools/publish-x.cs -- | ||
| --head "$DEPLOYED_REVISION" | ||
| --sitemap "$RUNNER_TEMP/deployed-sitemap.xml" | ||
| --state "$STATE_DIRECTORY/$STATE_FILE" | ||
| --account jackdaw-patio | ||
| --account-id "$X_ACCOUNT_USER_ID" | ||
|
|
||
| - name: Persist receipts and checkpoint | ||
| shell: bash | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| STATE_DIRECTORY: ${{ steps.state.outputs.directory }} | ||
| DEPLOYED_REVISION: ${{ steps.publication.outputs.deployed_revision }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| git -C "$STATE_DIRECTORY" add "$STATE_FILE" | ||
| if git -C "$STATE_DIRECTORY" diff --cached --quiet; then | ||
| echo "Publisher state already reflects $DEPLOYED_REVISION." | ||
| exit 0 | ||
| fi | ||
|
|
||
| git -C "$STATE_DIRECTORY" config user.name github-actions[bot] | ||
| git -C "$STATE_DIRECTORY" config user.email 41898282+github-actions[bot]@users.noreply.github.com | ||
| git -C "$STATE_DIRECTORY" commit -m "Record X publications through $DEPLOYED_REVISION" | ||
| gh auth setup-git | ||
| git -C "$STATE_DIRECTORY" push origin "HEAD:refs/heads/$STATE_BRANCH" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| # X Publication Runbook | ||
|
|
||
| Learning can optionally announce newly deployed `feed: true` documents through | ||
| the Jackdaw Patio X account. The `Publish Documentation` workflow invokes the | ||
| separate `Publish New Learning Content to X` reusable workflow only after the | ||
| Pages deployment succeeds. X availability therefore cannot block or roll back | ||
| the documentation deployment, RSS feed, sitemap, or IndexNow notification. | ||
|
|
||
| ## Publication boundary | ||
|
|
||
| The publisher fetches the public `deployment-revision.txt` and `sitemap.xml`, | ||
| then verifies that the deployed revision is a repository commit reachable from | ||
| `main`. It compares that revision with the cursor stored on | ||
| `automation/x-publisher-state`. | ||
|
|
||
| A document is selected only when it is Markdown under `docs/`, changes from | ||
| missing or `feed: false` to valid `feed: true` metadata, and has a canonical URL | ||
| in the deployed sitemap. Content edits, metadata updates, deletions, and Git | ||
| renames of already eligible documents do not create another post. RSS and X are | ||
| sibling consumers of source frontmatter; the X publisher does not read or | ||
| modify `feed.xml`. | ||
|
|
||
| ## Initial setup | ||
|
|
||
| 1. Create a protected GitHub Environment named `jackdaw-patio-x`. Restrict it to | ||
| trusted `main` deployments and add required reviewers if another maintainer | ||
| is available. | ||
| 2. Configure OAuth 1.0a user-context credentials as environment secrets: | ||
| `X_API_KEY`, `X_API_KEY_SECRET`, `X_ACCESS_TOKEN`, and | ||
| `X_ACCESS_TOKEN_SECRET`. | ||
| 3. Configure the expected numeric account identifier as the environment variable | ||
| `X_ACCOUNT_USER_ID`. | ||
| 4. Give the X application only the read/write permissions needed to inspect the | ||
| account timeline and create a post. | ||
| 5. Manually run the workflow with `dry_run` left enabled. This job has read-only | ||
| GitHub permissions, no protected environment, and no X credentials. | ||
| 6. Manually run with `dry_run` disabled to initialize | ||
| `automation/x-publisher-state` at the currently deployed revision. That first | ||
| run intentionally does not backfill the existing archive. | ||
|
|
||
| Do not place OAuth credentials in repository variables, source, workflow input, | ||
| logs, issues, or pull requests. The production API root is fixed in the tool and | ||
| cannot be overridden by repository or workflow input. | ||
|
|
||
| ## Delivery and retry behavior | ||
|
|
||
| The post text is deterministic: | ||
|
|
||
| ```text | ||
| New from ASI Backbone Learning: | ||
|
|
||
| {title} | ||
|
|
||
| {canonicalUrl} | ||
| ``` | ||
|
|
||
| Long titles are truncated at a Unicode text-element boundary while preserving | ||
| the canonical URL. Before every create call, the publisher searches recent | ||
| account posts for that URL. A successful create or reconciliation writes its | ||
| post ID and source blob SHA immediately. The cursor advances only after every | ||
| candidate succeeds. | ||
|
|
||
| HTTP `400` is treated as a publisher/content defect; `401` and `403` indicate a | ||
| credential or application-permission failure. `429`, `5xx`, and pre-response | ||
| connection failures receive bounded retries. An ambiguous timeout is reconciled | ||
| against recent posts before the run fails. X does not provide an application | ||
| idempotency key for this operation, so receipts plus URL reconciliation provide | ||
| effectively-once behavior with a small residual duplicate risk if X accepts a | ||
| post but neither its response nor the new post is observable during recovery. | ||
|
|
||
| The workflow stops on the first unresolved candidate. A later successful Pages | ||
| deployment or a controlled manual live run retries it because the durable cursor | ||
| has not advanced. | ||
|
|
||
| ## Manual validation and recovery | ||
|
|
||
| Run the complete offline contract suite without secrets or network access: | ||
|
|
||
| ```bash | ||
| dotnet run --file tools/publish-x.cs -- --self-test | ||
| ``` | ||
|
|
||
| Use the workflow's default manual dry run to inspect the candidates and exact | ||
| post text for the deployed revision. If a post succeeded but state persistence | ||
| failed, rerun the live workflow: recent-post reconciliation restores the missing | ||
| receipt before attempting a create. | ||
|
|
||
| For credential rotation, replace all four environment secrets as one coordinated | ||
| change, keep the account ID fixed, then run a dry run followed by a controlled | ||
| live run. For a wrong cursor or malformed manifest, do not delete receipts or | ||
| force-push the state branch. Repair it through a reviewed commit that preserves | ||
| valid receipts, then rerun manually. | ||
|
|
||
| ## Disabling publication | ||
|
|
||
| Disable the `Publish New Learning Content to X` workflow or remove the | ||
| `jackdaw-patio-x` environment approval. Documentation deployment remains | ||
| independent. Retain the state branch so re-enabling the publisher does not | ||
| reannounce previously recorded content. Revoke X credentials when the publisher | ||
| is retired rather than merely deleting the workflow secrets. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.