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
122 changes: 97 additions & 25 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,78 +1,150 @@
name: Create Github Release

on:
push:
workflow_run:
workflows: ["Tests"]
types:
- completed
branches:
- main

jobs:
CheckVersion:
if: github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main' && github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
outputs:
versionChanged: ${{ steps.check_version.outputs.changed }}
newVersion: ${{ steps.check_version.outputs.version }}
steps:
- name: Set up Git repository
uses: actions/checkout@v3
uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0

- name: Set up node
uses: actions/setup-node@v3
with:
node-version: 18
fetch-tags: true

- name: Check if version has been updated
# A release is due when the package.json version has no matching git tag yet
- name: Check whether this version still needs releasing
id: check_version
uses: EndBug/version-check@v2
with:
diff-search: true

- name: Log version change
if: steps.check_version.outputs.changed == 'true'
run: 'echo "Version change found in commit ${{ steps.check_version.outputs.commit }}! New version: ${{ steps.check_version.outputs.version }}"'
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if git rev-parse -q --verify "refs/tags/v$VERSION" >/dev/null; then
echo "changed=false" >> "$GITHUB_OUTPUT"
echo "v$VERSION is already tagged - nothing to release."
else
echo "changed=true" >> "$GITHUB_OUTPUT"
echo "v$VERSION has no tag yet - a release will be created."
fi

CreateRelease:
needs: CheckVersion
if: needs.CheckVersion.outputs.versionChanged == 'true'
runs-on: ubuntu-latest

steps:
- name: Set up Git repository
uses: actions/checkout@v3
uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}
fetch-depth: 0
fetch-tags: true

- name: Copy required files to release_files folder
run: |
mkdir release_files
cp ./LICENSE ./release_files/LICENSE
cp ./README.md ./release_files/README.md
cp -r ./bin ./release_files/bin
cp -r ./js ./release_files/js
cp ./config.default.json ./release_files/config.default.json
cp ./config.schema.json ./release_files/config.schema.json
cp ./index.js ./release_files/index.js
cp ./package-lock.json ./release_files/package-lock.json
cp ./package.json ./release_files/package.json

- name: Create .zip of required files
- name: Create .zip of required files
run: |
cd release_files
zip -r ../Game.Pass.API.zip *

- name: Create Release notes
run: |
echo "## What's Changed

" > CHANGELOG.md
LAST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)
git log --pretty=format:'* %s' $LAST_TAG..HEAD >> CHANGELOG.md
cat > CHANGELOG.md <<'EOF'
> **Recommended:** install from npm with `npm install -g game-pass-api` (or run it directly with `npx game-pass-api`) to always get the latest version. See the [README](https://github.com/NikkelM/Game-Pass-API#readme) for all options.

## What's Changed

EOF
LAST_TAG=$(git describe --tags "$(git rev-list --tags --max-count=1)")
git log --pretty=format:'* %s' "$LAST_TAG"..HEAD >> CHANGELOG.md
cat >> CHANGELOG.md <<'EOF'

## Using the release asset

To run this version offline, download the attached `Game.Pass.API.zip`, unzip it, and from inside the folder run:

```bash
npm ci # install dependencies from the bundled lockfile
node bin/cli.js init # create a config.json interactively (or add one yourself)
node bin/cli.js # fetch Game Pass data using ./config.json
```

Requires [Node.js](https://nodejs.org) 22.13 or newer. Run `node bin/cli.js --help` for all commands.
EOF

- name: Create Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v3
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ needs.CheckVersion.outputs.newVersion }}
name: v${{ needs.CheckVersion.outputs.newVersion }}
files: Game.Pass.API.zip
body_path: CHANGELOG.md
body_path: CHANGELOG.md

PublishNpm:
needs: CheckVersion
if: needs.CheckVersion.outputs.versionChanged == 'true'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for npm Trusted Publishing (OIDC)
steps:
- name: Set up Git repository
uses: actions/checkout@v7
with:
ref: ${{ github.event.workflow_run.head_sha }}

- name: Set up node
uses: actions/setup-node@v6
with:
node-version: 24
registry-url: 'https://registry.npmjs.org'

# Trusted Publishing (token-free OIDC) needs a recent npm; the bundled one may be too old
- name: Update npm
run: npm install -g npm@latest

- name: Install dependencies
run: npm ci

# Publishing is idempotent: skip if this version is already on npm. This prevents a failed
# job on the first release (published manually to bootstrap Trusted Publishing) and on any
# re-run, since npm versions are immutable and cannot be published over
- name: Skip if this version is already on npm
id: published
run: |
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
if [ -n "$(npm view "$NAME@$VERSION" version 2>/dev/null || true)" ]; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Version $VERSION is already on npm - skipping publish."
else
echo "exists=false" >> "$GITHUB_OUTPUT"
fi

# No NPM_TOKEN needed: authentication and provenance are handled automatically via
# OIDC Trusted Publishing, configured for this repo on npmjs.com
- name: Publish to npm
if: steps.published.outputs.exists == 'false'
run: npm publish
74 changes: 61 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,67 @@

![Game Pass API banner](images/GamePassApiBanner.png)

This project provides a quick and easy way to get a list of all games currently available on Xbox Game Pass on a given platform (Console, PC or through EA Play) in a given region.
[![npm version](https://img.shields.io/npm/v/game-pass-api)](https://www.npmjs.com/package/game-pass-api)
[![Tests](https://github.com/NikkelM/Game-Pass-API/actions/workflows/test.yml/badge.svg)](https://github.com/NikkelM/Game-Pass-API/actions/workflows/test.yml)
<!-- [![npm downloads](https://img.shields.io/npm/dt/game-pass-api)](https://www.npmjs.com/package/game-pass-api) -->

Using the configuration file, the type and format of many properties can be customized, from simply getting the game's names all the way to fetching store prices at the moment of the request.
Get a list of all games currently available on Xbox Game Pass (Console, PC or EA Play) in any region, with their properties formatted just the way you need - from just the game names all the way to live store prices at the moment of the request.

Take a look at the section below to learn how to use the configuration file, or take a look at the [examples](examples) and work from there.
## Table of contents

## Setup
- [Installation](#installation)
- [Usage](#usage)
- [Output](#output)
- [Configuration](#configuration)
- [Examples](https://github.com/NikkelM/Game-Pass-API/tree/main/examples)
- [Feedback](#feedback)

Run `npm install` to install the required dependencies first.
## Installation

Following this, create a `config.json` file in the root directory of the project and fill it with your desired [configuration](#configuration).
Run it on demand without installing anything (requires [Node.js](https://nodejs.org) 22.13 or newer):

```bash
npx game-pass-api
```

Or install it globally to get a persistent `game-pass-api` command:

```bash
npm install -g game-pass-api
```

## Usage

After providing the `config.json` [configuration](#configuration) file, you can run the script using
The tool is driven by a `config.json` in your current directory.
The quickest way to create one is the interactive wizard:

```bash
game-pass-api init
```

It asks a few questions (markets, language, platforms, and which properties to include) and writes a `config.json`, then offers to run it right away.

You can also write the `config.json` by hand - see [Configuration](#configuration) - and then run the tool:

```bash
node index.js
game-pass-api
```

You will find the resulting data in the created `output` folder.
By default it reads `./config.json`; pass `--config <path>` to point at a different file.
Run `game-pass-api --help` to see every command.

Unsure how it works? Take a look at the [examples](examples) for some inspiration.
> Configuration files are validated against a JSON schema (`config.schema.json`, shipped with the package).
> Add `"$schema": "config.schema.json"` to your `config.json`, with a copy of the schema next to it, and your editor will flag mistakes as you type.

Unsure where to start? Take a look at the [examples](https://github.com/NikkelM/Game-Pass-API/tree/main/examples) and work from there.

## Output

The results are written to an `output/` folder in your current directory (change it with the `outputDirectory` config field or `-o, --out <dir>`), with one file per platform and market (for example `output/formattedGameProperties_console_US.json`).
When `keepCompleteProperties` is enabled, the full unfiltered API response is written alongside it (for example `output/completeGameProperties_console_US.json`).

Those complete files can be re-formatted without fetching again: `game-pass-api run --from <dir>` (for example `--from output`) reads the saved `completeGameProperties_*.json` files for your configured markets and platforms and re-applies the current `outputFormat` and `includedProperties`.
Use it to re-slice a saved snapshot into a different shape offline, without hitting the API.

## Configuration

Expand All @@ -46,7 +84,7 @@ The schema can be found in the `config.schema.json` file and used within your `c

The following is a list of all configuration items, their defaults in the `config.default.json` and the values you can assign to them.

If a given property is not present in the configuration file, it will automatically be assumed to have a value of `false` (or equivalent, depending on the property type), and not be included in the output.
If a property under `includedProperties` is not present in the configuration file, it is left out of the output; any other optional property falls back to the default listed in its table below.

#### Top-level properties

Expand Down Expand Up @@ -126,6 +164,16 @@ Whether to keep the original, complete list of properties for the fetched games.
| `boolean` | `false` | `true` or `false` | No |
</details>

<details>
<summary><code>outputDirectory</code></summary>

The directory to write the output files to, relative to the current directory. Can also be set on the command line with `-o, --out <dir>`.

| Type | Default | Possible values | Required |
| --- | --- | --- | --- |
| `string` | `"output"` | Any directory path | No |
</details>

#### includedProperties

If any of these properties are omitted, they will not be included in the output. The `Default` column indicates the default applied when using the `config.default.json` file.
Expand Down Expand Up @@ -384,7 +432,7 @@ Whether to include the game's price information. The currency that is used is de
"MSRP",
"WholesalePrice"
],
"missingPricePolicy": "useZero"
"missingPricePolicy": "useNull"
}
```

Expand Down Expand Up @@ -431,4 +479,4 @@ If you have any question, feedback or feature requests, feel free to open an [is

## Disclaimer

*This unofficial project is not affiliated with Microsoft or Xbox in any way. The data provided when using this project is obtained from the public Xbox catalog API and is not guaranteed to be accurate or up-to-date.*
*This unofficial project is not affiliated with Microsoft or XBOX in any way. The data provided when using this project is obtained from the public XBOX catalog API and is not guaranteed to be accurate or up-to-date.*
52 changes: 52 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env node

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { Command } from 'commander';

import { loadConfig } from '../js/utils.js';
import { run } from '../js/gamePass.js';
import { runWizard } from '../js/wizard.js';

const packageRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));

const program = new Command();

program
.name('game-pass-api')
.description('Fetch all games on Xbox Game Pass (Console, PC and EA Play) with configurable properties, driven by a config.json.')
.version(pkg.version);

program
.command('run', { isDefault: true })
.description('Fetch Game Pass data using a configuration file')
.option('-c, --config <path>', 'path to a config.json (defaults to ./config.json)')
.option('--from <dir>', 're-format previously-saved completeGameProperties_*.json files in <dir> instead of fetching (needs an earlier run with keepCompleteProperties)')
.option('-o, --out <dir>', 'directory to write output files to (overrides outputDirectory; default: output)')
.addHelpText('after', '\nThe configuration comes from a config.json in the current directory (or --config <path>).\nRun "game-pass-api init" to create one interactively, or see the README and config.schema.json for every option.')
.action(async (options) => {
const config = loadConfig(options.config);
if (options.out) {
config.outputDirectory = options.out;
}
await run(config, { fromDirectory: options.from });
});

program
.command('init')
.description('Interactively create a configuration file')
.option('-o, --output <path>', 'where to write the configuration file', 'config.json')
.action(async (options) => {
await runWizard(options.output);
});

program.showHelpAfterError('(run with --help to see available commands)');

try {
await program.parseAsync(process.argv);
} catch (error) {
console.error('Error: ' + (error?.message ?? error));
process.exit(1);
}
1 change: 1 addition & 0 deletions config.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"outputFormat": "array",
"treatEmptyStringsAsNull": true,
"keepCompleteProperties": false,
"outputDirectory": "output",
"includedProperties": {
"productTitle": true,
"productId": false,
Expand Down
7 changes: 6 additions & 1 deletion config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,11 @@
"type": "boolean",
"default": false
},
"outputDirectory": {
"description": "The directory to write the output files to, relative to the current directory. Can also be set with the -o, --out command-line option.",
"type": "string",
"default": "output"
},
"includedProperties": {
"description": "The properties that should be contained in the filtered version of the API response.",
"type": "object",
Expand Down Expand Up @@ -605,7 +610,7 @@
"MSRP",
"WholesalePrice"
],
"missingPricePolicy": "useZero"
"missingPricePolicy": "useNull"
},
"properties": {
"enabled": {
Expand Down
11 changes: 10 additions & 1 deletion examples/completionist/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@

This example configuration fetches as many properties as possible for a number of different markets.

The values were fetched on 2021-03-01.
Generate these results by running the following from this folder:

```bash
npx game-pass-api
```

The values were fetched on 2026-07-14.

> The output files here are trimmed to the same few games across both markets, to show the format and how properties like pricing differ per region.
> The command above produces the full catalog for every market and platform.
Loading